diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 72c88b1e92..a0c8e0d555 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -104,127 +104,39 @@ namespace osu.Game.Online.API
///
private int failureCount;
+ ///
+ /// The main API thread loop, which will continue to run until the game is shut down.
+ ///
private void run()
{
while (!cancellationToken.IsCancellationRequested)
{
- switch (State.Value)
+ if (state.Value == APIState.Failing)
{
- case APIState.Failing:
- //todo: replace this with a ping request.
- log.Add(@"In a failing state, waiting a bit before we try again...");
- Thread.Sleep(5000);
+ // To recover from a failing state, falling through and running the full reconnection process seems safest for now.
+ // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
+ log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again...");
+ Thread.Sleep(5000);
+ }
- if (!IsLoggedIn) goto case APIState.Connecting;
+ // Ensure that we have valid credentials.
+ // If not, setting the offline state will allow the game to prompt the user to provide new credentials.
+ if (!HasLogin)
+ {
+ state.Value = APIState.Offline;
+ Thread.Sleep(50);
+ continue;
+ }
- if (queue.Count == 0)
- {
- log.Add(@"Queueing a ping request");
- Queue(new GetUserRequest());
- }
+ Debug.Assert(HasLogin);
- break;
+ // Ensure that we are in an online state. If not, attempt a connect.
+ if (state.Value != APIState.Online)
+ {
+ attemptConnect();
- case APIState.Offline:
- case APIState.Connecting:
- // work to restore a connection...
- if (!HasLogin)
- {
- state.Value = APIState.Offline;
- Thread.Sleep(50);
- continue;
- }
-
- state.Value = APIState.Connecting;
-
- if (localUser.IsDefault)
- {
- // Show a placeholder user if saved credentials are available.
- // This is useful for storing local scores and showing a placeholder username after starting the game,
- // until a valid connection has been established.
- setLocalUser(new APIUser
- {
- Username = ProvidedUsername,
- });
- }
-
- // save the username at this point, if the user requested for it to be.
- config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
-
- if (!authentication.HasValidAccessToken)
- {
- LastLoginError = null;
-
- try
- {
- authentication.AuthenticateWithLogin(ProvidedUsername, password);
- }
- catch (Exception e)
- {
- //todo: this fails even on network-related issues. we should probably handle those differently.
- LastLoginError = e;
- log.Add(@"Login failed!");
- password = null;
- authentication.Clear();
- continue;
- }
- }
-
- var userReq = new GetUserRequest();
-
- userReq.Failure += ex =>
- {
- if (ex is APIException)
- {
- LastLoginError = ex;
- log.Add("Login failed on local user retrieval!");
- Logout();
- }
- else if (ex is WebException webException && webException.Message == @"Unauthorized")
- {
- log.Add(@"Login no longer valid");
- Logout();
- }
- else
- failConnectionProcess();
- };
- userReq.Success += user =>
- {
- // todo: save/pull from settings
- user.Status.Value = new UserStatusOnline();
-
- setLocalUser(user);
-
- //we're connected!
- state.Value = APIState.Online;
- failureCount = 0;
- };
-
- if (!handleRequest(userReq))
- {
- failConnectionProcess();
- continue;
- }
-
- // getting user's friends is considered part of the connection process.
- var friendsReq = new GetFriendsRequest();
-
- friendsReq.Failure += _ => failConnectionProcess();
- friendsReq.Success += res => friends.AddRange(res);
-
- if (!handleRequest(friendsReq))
- {
- failConnectionProcess();
- continue;
- }
-
- // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
- // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
- // before actually going online.
- while (State.Value > APIState.Offline && State.Value < APIState.Online)
- Thread.Sleep(500);
-
- break;
+ if (state.Value != APIState.Online)
+ continue;
}
// hard bail if we can't get a valid access token.
@@ -234,31 +146,132 @@ namespace osu.Game.Online.API
continue;
}
- while (true)
- {
- APIRequest req;
-
- lock (queue)
- {
- if (queue.Count == 0) break;
-
- req = queue.Dequeue();
- }
-
- handleRequest(req);
- }
-
+ processQueuedRequests();
Thread.Sleep(50);
}
+ }
- void failConnectionProcess()
+ ///
+ /// Dequeue from the queue and run each request synchronously until the queue is empty.
+ ///
+ private void processQueuedRequests()
+ {
+ while (true)
{
- // if something went wrong during the connection process, we want to reset the state (but only if still connecting).
- if (State.Value == APIState.Connecting)
- state.Value = APIState.Failing;
+ APIRequest req;
+
+ lock (queue)
+ {
+ if (queue.Count == 0) return;
+
+ req = queue.Dequeue();
+ }
+
+ handleRequest(req);
}
}
+ ///
+ /// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
+ ///
+ ///
+ /// This method takes control of and transitions from to either
+ /// - (successful connection)
+ /// - (failed connection but retrying)
+ /// - (failed and can't retry, clear credentials and require user interaction)
+ ///
+ /// Whether the connection attempt was successful.
+ private void attemptConnect()
+ {
+ state.Value = APIState.Connecting;
+
+ if (localUser.IsDefault)
+ {
+ // Show a placeholder user if saved credentials are available.
+ // This is useful for storing local scores and showing a placeholder username after starting the game,
+ // until a valid connection has been established.
+ setLocalUser(new APIUser
+ {
+ Username = ProvidedUsername,
+ });
+ }
+
+ // save the username at this point, if the user requested for it to be.
+ config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
+
+ if (!authentication.HasValidAccessToken)
+ {
+ LastLoginError = null;
+
+ try
+ {
+ authentication.AuthenticateWithLogin(ProvidedUsername, password);
+ }
+ catch (Exception e)
+ {
+ //todo: this fails even on network-related issues. we should probably handle those differently.
+ LastLoginError = e;
+ log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!");
+
+ Logout();
+ return;
+ }
+ }
+
+ var userReq = new GetUserRequest();
+ userReq.Failure += ex =>
+ {
+ if (ex is APIException)
+ {
+ LastLoginError = ex;
+ log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
+ Logout();
+ }
+ else if (ex is WebException webException && webException.Message == @"Unauthorized")
+ {
+ log.Add(@"Login no longer valid");
+ Logout();
+ }
+ else
+ {
+ state.Value = APIState.Failing;
+ }
+ };
+ userReq.Success += user =>
+ {
+ // todo: save/pull from settings
+ user.Status.Value = new UserStatusOnline();
+
+ setLocalUser(user);
+
+ // we're connected!
+ state.Value = APIState.Online;
+ failureCount = 0;
+ };
+
+ if (!handleRequest(userReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ var friendsReq = new GetFriendsRequest();
+ friendsReq.Failure += _ => state.Value = APIState.Failing;
+ friendsReq.Success += res => friends.AddRange(res);
+
+ if (!handleRequest(friendsReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
+ // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
+ // before actually going online.
+ while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested)
+ Thread.Sleep(500);
+ }
+
public void Perform(APIRequest request)
{
try