diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 46f7c461f8..7e995f2dde 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
- [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
+ [TestCase(6.7115569159190587d, 206, "diffcalc-test")]
+ [TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
- [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
+ [TestCase(8.9757300665532966d, 206, "diffcalc-test")]
+ [TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
- [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
+ [TestCase(6.7115569159190587d, 239, "diffcalc-test")]
+ [TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 76d5ccf682..6d1b4d1a15 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 2.0;
- private const double slider_multiplier = 1.5;
+ private const double acute_angle_multiplier = 1.95;
+ private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
///
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
}
- if (osuLastObj.TravelTime != 0)
+ if (osuLastObj.BaseObject is Slider)
{
// Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
index 3b0826394c..fcf4179a3b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
@@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2;
+ private const double min_velocity = 0.5;
+ private const double slider_multiplier = 1.3;
+
///
/// Evaluates the difficulty of memorising and hitting an object, based on:
///
- /// - distance between the previous and current object,
+ /// - distance between a number of previous objects and the current object,
/// - the visual opacity of the current object,
+ /// - length and speed of the current object (for sliders),
/// - and whether the hidden mod is enabled.
///
///
@@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden)
result *= 1.0 + hidden_bonus;
+ double sliderBonus = 0.0;
+
+ if (osuCurrent.BaseObject is Slider osuSlider)
+ {
+ // Invert the scaling factor to determine the true travel distance independent of circle size.
+ double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
+
+ // Reward sliders based on velocity.
+ sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
+
+ // Longer sliders require more memorisation.
+ sliderBonus *= pixelTravelDistance;
+
+ // Nerf sliders with repeats, as less memorisation is required.
+ if (osuSlider.RepeatCount > 0)
+ sliderBonus /= (osuSlider.RepeatCount + 1);
+ }
+
+ result += sliderBonus * slider_multiplier;
+
return result;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 9f4a405113..0ebfb9a283 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax))
+ {
+ aimRating *= 0.9;
speedRating = 0.0;
+ flashlightRating *= 0.7;
+ }
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
@@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
);
- double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
+ double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index c3b7834009..3c82c2dc33 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
+ public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
@@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
- double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+ double multiplier = PERFORMANCE_BASE_MULTIPLIER;
if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
{
- // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
- effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
+ // https://www.desmos.com/calculator/bc9eybdthb
+ // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
+ // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
+ double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
+ double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
- multiplier *= 0.6;
+ // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
+ effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
double aimValue = computeAimValue(score, osuAttributes);
@@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0)
- approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
+ approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
+
+ if (score.Mods.Any(h => h is OsuModRelax))
+ approachRateFactor = 0.0;
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
@@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
+ if (score.Mods.Any(h => h is OsuModRelax))
+ return 0.0;
+
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
- speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
+ speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue;
}
@@ -266,6 +278,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
- private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index c8646ec456..c7c5650184 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
- private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+ ///
+ /// A distance by which all distances should be scaled in order to assume a uniform circle size.
+ ///
+ public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+
private const int min_delta_time = 25;
- private const float maximum_slider_radius = normalised_radius * 2.4f;
- private const float assumed_slider_radius = normalised_radius * 1.8f;
+ private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
+ private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; }
///
- /// The time taken to travel through , with a minimum value of 25ms for a non-zero distance.
+ /// The time taken to travel through , with a minimum value of 25ms for objects.
///
public double TravelTime { get; private set; }
@@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider)
{
computeSliderCursorPosition(currentSlider);
- TravelDistance = currentSlider.LazyTravelDistance;
+ // Bonus for repeat sliders until a better per nested object strain system can be achieved.
+ TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
}
@@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
- float scalingFactor = normalised_radius / (float)BaseObject.Radius;
+ float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
@@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition;
- double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
+ double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{
@@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
- requiredMovement = normalised_radius;
+ requiredMovement = NORMALISED_RADIUS;
}
if (currMovementLength > requiredMovement)
@@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition;
}
-
- slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
}
private Vector2 getEndCursorPosition(OsuHitObject hitObject)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 9b1fbf9a2e..38e0e5b677 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
- private double skillMultiplier => 23.25;
+ private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index a0dff8a2ea..ad53f6d90f 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Input.Bindings
private InputManager? parentInputManager;
- public GlobalActionContainer(OsuGameBase game)
+ public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{
if (game is IKeyBindingHandler)
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 42133160ca..a0c8e0d555 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -104,131 +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.
- localUser.Value = 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 += u =>
- {
- localUser.Value = u;
-
- // todo: save/pull from settings
- localUser.Value.Status.Value = new UserStatusOnline();
-
- 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);
-
- //we're connected!
- state.Value = APIState.Online;
- };
-
- 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.
@@ -238,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
@@ -338,8 +347,7 @@ namespace osu.Game.Online.API
if (req.CompletionState != APIRequestCompletionState.Completed)
return false;
- // we could still be in initialisation, at which point we don't want to say we're Online yet.
- if (IsLoggedIn) state.Value = APIState.Online;
+ // Reset failure count if this request succeeded.
failureCount = 0;
return true;
}
@@ -453,7 +461,7 @@ namespace osu.Game.Online.API
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>
{
- localUser.Value = createGuestUser();
+ setLocalUser(createGuestUser());
friends.Clear();
});
@@ -463,6 +471,8 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser();
+ private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index 812aa7f09f..a90b11e354 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -13,19 +13,16 @@ namespace osu.Game.Online.API
{
///
/// The local user.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindable LocalUser { get; }
///
/// The user's friends.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindableList Friends { get; }
///
/// The current user's activity.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindable Activity { get; }