mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'master' into open-profile-hotkey
This commit is contained in:
commit
667b700a28
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of memorising and hitting an object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>distance between the previous and current object,</description></item>
|
||||
/// <item><description>distance between a number of previous objects and the current object,</description></item>
|
||||
/// <item><description>the visual opacity of the current object,</description></item>
|
||||
/// <item><description>length and speed of the current object (for sliders),</description></item>
|
||||
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
/// <summary>
|
||||
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
|
||||
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
|
||||
/// </summary>
|
||||
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)
|
||||
|
@ -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);
|
||||
|
@ -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<GlobalAction>)
|
||||
|
@ -104,131 +104,39 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
private int failureCount;
|
||||
|
||||
/// <summary>
|
||||
/// The main API thread loop, which will continue to run until the game is shut down.
|
||||
/// </summary>
|
||||
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<bool>(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()
|
||||
/// <summary>
|
||||
/// Dequeue from the queue and run each request synchronously until the queue is empty.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
|
||||
/// - <see cref="APIState.Online"/> (successful connection)
|
||||
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
|
||||
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
|
||||
/// </remarks>
|
||||
/// <returns>Whether the connection attempt was successful.</returns>
|
||||
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<bool>(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);
|
||||
|
@ -13,19 +13,16 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
/// <summary>
|
||||
/// The local user.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindable<APIUser> LocalUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's friends.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindableList<APIUser> Friends { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current user's activity.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindable<UserActivity> Activity { get; }
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user