diff --git a/osu.Android.props b/osu.Android.props
index 196d122a2a..c78dfb6a55 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index d06c4b6746..5fb09c0cef 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -69,7 +69,6 @@ namespace osu.Desktop
/// Allow a maximum of one unhandled exception, per second of execution.
///
///
- ///
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index c81710ed18..26e5d381e2 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the sample info list at a point in time.
///
/// The time to retrieve the sample info list from.
- ///
private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 856b6554b9..0ba775e5c7 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private void runSpmTest(Mod mod)
{
- SpinnerSpmCounter spmCounter = null;
+ SpinnerSpmCalculator spmCalculator = null;
CreateModTest(new ModTestData
{
@@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
- AddUntilStep("fetch SPM counter", () =>
+ AddUntilStep("fetch SPM calculator", () =>
{
- spmCounter = this.ChildrenOfType().SingleOrDefault();
- return spmCounter != null;
+ spmCalculator = this.ChildrenOfType().SingleOrDefault();
+ return spmCalculator != null;
});
- AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
+ AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 7df5ca0f7c..24e69703a6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Beatmap = singleSpinnerBeatmap,
PassCondition = () =>
{
- var counter = Player.ChildrenOfType().SingleOrDefault();
- return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
+ var counter = Player.ChildrenOfType().SingleOrDefault();
+ return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
}
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
new file mode 100644
index 0000000000..73753554f7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ac8d5c81bc..14c709cae1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
double estimatedSpm = 0;
addSeekStep(1000);
- AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
+ AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
addSeekStep(1000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
}
[TestCase(0.5)]
@@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spinner state", () =>
{
expectedProgress = drawableSpinner.Progress;
- expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ expectedSpm = drawableSpinner.SpinsPerMinute.Value;
});
addSeekStep(0);
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
- AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 39e78a14aa..3a4753761a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; }
- public SpinnerSpmCounter SpmCounter { get; private set; }
+
+ private SpinnerSpmCalculator spmCalculator;
private Container ticks;
private PausableSkinnableSound spinningSample;
@@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public IBindable GainedBonus => gainedBonus;
- private readonly Bindable gainedBonus = new Bindable();
+ private readonly Bindable gainedBonus = new BindableDouble();
+
+ ///
+ /// The number of spins per minute this spinner is spinning at, for display purposes.
+ ///
+ public readonly IBindable SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160;
@@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
+ spmCalculator = new SpinnerSpmCalculator
+ {
+ Result = { BindTarget = SpinsPerMinute },
+ },
ticks = new Container(),
new AspectContainer
{
@@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RotationTracker = new SpinnerRotationTracker(this)
}
},
- SpmCounter = new SpinnerSpmCounter
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = 120,
- Alpha = 0
- },
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
}
- };
+ });
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
}
@@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- protected override void UpdateStartTimeStateTransforms()
- {
- base.UpdateStartTimeStateTransforms();
-
- if (Result?.TimeStarted is double startTime)
- {
- using (BeginAbsoluteSequence(startTime))
- fadeInCounter();
- }
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
@@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- {
- Result.TimeStarted ??= Time.Current;
- fadeInCounter();
- }
+ if (Result.TimeStarted == null && RotationTracker.Tracking)
+ Result.TimeStarted = Time.Current;
// don't update after end time to avoid the rate display dropping during fade out.
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
if (Time.Current <= HitObject.EndTime)
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ spmCalculator.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
- private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
-
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
index 891821fe2f..ae8c03dad1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private OsuSpriteText bonusCounter;
+ private Container spmContainer;
+ private OsuSpriteText spmCounter;
+
public DefaultSpinner()
{
RelativeSizeAxes = Axes.Both;
@@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Y = -120,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 120,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Numeric.With(size: 24)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Numeric.With(size: 12),
+ Y = 30
+ }
+ }
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
protected override void LoadComplete()
{
@@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
});
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
similarity index 61%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
index 69355f624b..a5205bbb8c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
@@ -1,77 +1,37 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class SpinnerSpmCounter : Container
+ public class SpinnerSpmCalculator : Component
{
+ private readonly Queue records = new Queue();
+ private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
+
+ ///
+ /// The resultant spins per minute value, which is updated via .
+ ///
+ public IBindable Result => result;
+
+ private readonly Bindable result = new BindableDouble();
+
[Resolved]
private DrawableHitObject drawableSpinner { get; set; }
- private readonly OsuSpriteText spmText;
-
- public SpinnerSpmCounter()
- {
- Children = new Drawable[]
- {
- spmText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"0",
- Font = OsuFont.Numeric.With(size: 24)
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"SPINS PER MINUTE",
- Font = OsuFont.Numeric.With(size: 12),
- Y = 30
- }
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.HitObjectApplied += resetState;
}
- private double spm;
-
- public double SpinsPerMinute
- {
- get => spm;
- private set
- {
- if (value == spm) return;
-
- spm = value;
- spmText.Text = Math.Truncate(value).ToString(@"#0");
- }
- }
-
- private struct RotationRecord
- {
- public float Rotation;
- public double Time;
- }
-
- private readonly Queue records = new Queue();
- private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
-
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue();
- SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
+ result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
@@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void resetState(DrawableHitObject hitObject)
{
- SpinsPerMinute = 0;
+ result.Value = 0;
records.Clear();
}
@@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState;
}
+
+ private struct RotationRecord
+ {
+ public float Rotation;
+ public double Time;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 064b7a4680..7eb6898abc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected const float SPRITE_SCALE = 0.625f;
+ private const float spm_hide_offset = 50f;
+
protected DrawableSpinner DrawableSpinner { get; private set; }
private Sprite spin;
@@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private LegacySpriteText bonusCounter;
+ private Sprite spmBackground;
+ private LegacySpriteText spmCounter;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
@@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 299,
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
+ spmBackground = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ Texture = source.GetTexture("spinner-rpm"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Position = new Vector2(-87, 445 + spm_hide_offset),
+ },
+ spmCounter = new LegacySpriteText(source, LegacyFont.Score)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(SPRITE_SCALE * 0.9f),
+ Position = new Vector2(80, 448 + spm_hide_offset),
+ }.With(s => s.Font = s.Font.With(fixedWidth: false)),
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
private readonly Bindable completed = new Bindable();
@@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
});
+ spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (drawableHitObject)
{
case DrawableSpinner d:
- double fadeOutLength = Math.Min(400, d.HitObject.Duration);
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
+ {
+ spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ }
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
- spin.FadeOutFromOne(fadeOutLength);
+ double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
+
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 77f910c144..3afb7481b1 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -11,7 +11,10 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
namespace osu.Game.Tests.Online
{
@@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online
Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
}
+ [Test]
+ public void TestDeserialiseScoreInfoWithEmptyMods()
+ {
+ var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(deserialised?.Mods.Length, Is.Zero);
+ }
+
+ [Test]
+ public void TestDeserialiseScoreInfoWithCustomModSetting()
+ {
+ var score = new ScoreInfo
+ {
+ Ruleset = new OsuRuleset().RulesetInfo,
+ Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
+ };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 1ee848b902..b6c06bb149 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
@@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+ private OsuConfigManager config;
+
public TestSceneMultiplayerGameplayLeaderboard()
{
base.Content.Children = new Drawable[]
@@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
+ }
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
}
+ [Test]
+ public void TestChangeScoringMode()
+ {
+ AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5);
+ AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
+ AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+ }
+
public class TestMultiplayerStreaming : SpectatorStreamingClient
{
public new BindableList PlayingUsers => (BindableList)base.PlayingUsers;
@@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break;
}
- ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty()));
+ ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 839118de2f..caa731f985 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
new file mode 100644
index 0000000000..3b2cfb1c7b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
@@ -0,0 +1,229 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
+using osu.Game.Database;
+using osu.Game.Online;
+using osu.Game.Online.Spectator;
+using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
+ {
+ [Cached(typeof(SpectatorStreamingClient))]
+ private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
+
+ [Cached(typeof(UserLookupCache))]
+ private UserLookupCache lookupCache = new TestUserLookupCache();
+
+ protected override Container Content => content;
+ private readonly Container content;
+
+ private readonly Dictionary clocks = new Dictionary
+ {
+ { 55, new ManualClock() },
+ { 56, new ManualClock() }
+ };
+
+ public TestSceneMultiplayerSpectatorLeaderboard()
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ streamingClient,
+ lookupCache,
+ content = new Container { RelativeSizeAxes = Axes.Both }
+ });
+ }
+
+ [SetUpSteps]
+ public new void SetUpSteps()
+ {
+ MultiplayerSpectatorLeaderboard leaderboard = null;
+
+ AddStep("reset", () =>
+ {
+ Clear();
+
+ foreach (var (userId, clock) in clocks)
+ {
+ streamingClient.EndPlay(userId, 0);
+ clock.CurrentTime = 0;
+ }
+ });
+
+ AddStep("create leaderboard", () =>
+ {
+ foreach (var (userId, _) in clocks)
+ streamingClient.StartPlay(userId, 0);
+
+ Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
+
+ var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var scoreProcessor = new OsuScoreProcessor();
+ scoreProcessor.ApplyBeatmap(playable);
+
+ LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
+ });
+
+ AddUntilStep("wait for load", () => leaderboard.IsLoaded);
+
+ AddStep("add clock sources", () =>
+ {
+ foreach (var (userId, clock) in clocks)
+ leaderboard.AddClock(userId, clock);
+ });
+ }
+
+ [Test]
+ public void TestLeaderboardTracksCurrentTime()
+ {
+ AddStep("send frames", () =>
+ {
+ // For user 55, send frames in sets of 1.
+ // For user 56, send frames in sets of 10.
+ for (int i = 0; i < 100; i++)
+ {
+ streamingClient.SendFrames(55, i, 1);
+
+ if (i % 10 == 0)
+ streamingClient.SendFrames(56, i, 10);
+ }
+ });
+
+ assertCombo(55, 1);
+ assertCombo(56, 10);
+
+ // Advance to a point where only user 55's frame changes.
+ setTime(500);
+ assertCombo(55, 5);
+ assertCombo(56, 10);
+
+ // Advance to a point where both user's frame changes.
+ setTime(1100);
+ assertCombo(55, 11);
+ assertCombo(56, 20);
+
+ // Advance user 56 only to a point where its frame changes.
+ setTime(56, 2100);
+ assertCombo(55, 11);
+ assertCombo(56, 30);
+
+ // Advance both users beyond their last frame
+ setTime(101 * 100);
+ assertCombo(55, 100);
+ assertCombo(56, 100);
+ }
+
+ [Test]
+ public void TestNoFrames()
+ {
+ assertCombo(55, 0);
+ assertCombo(56, 0);
+ }
+
+ private void setTime(double time) => AddStep($"set time {time}", () =>
+ {
+ foreach (var (_, clock) in clocks)
+ clock.CurrentTime = time;
+ });
+
+ private void setTime(int userId, double time)
+ => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
+
+ private void assertCombo(int userId, int expectedCombo)
+ => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
+
+ private class TestSpectatorStreamingClient : SpectatorStreamingClient
+ {
+ private readonly Dictionary userBeatmapDictionary = new Dictionary();
+ private readonly Dictionary userSentStateDictionary = new Dictionary();
+
+ public TestSpectatorStreamingClient()
+ : base(new DevelopmentEndpointConfiguration())
+ {
+ }
+
+ public void StartPlay(int userId, int beatmapId)
+ {
+ userBeatmapDictionary[userId] = beatmapId;
+ userSentStateDictionary[userId] = false;
+ sendState(userId, beatmapId);
+ }
+
+ public void EndPlay(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = false;
+ }
+
+ public void SendFrames(int userId, int index, int count)
+ {
+ var frames = new List();
+
+ for (int i = index; i < index + count; i++)
+ {
+ var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
+ frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
+ }
+
+ var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
+ ((ISpectatorClient)this).UserSentFrames(userId, bundle);
+ if (!userSentStateDictionary[userId])
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ public override void WatchUser(int userId)
+ {
+ if (userSentStateDictionary[userId])
+ {
+ // usually the server would do this.
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ base.WatchUser(userId);
+ }
+
+ private void sendState(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = true;
+ }
+ }
+
+ private class TestUserLookupCache : UserLookupCache
+ {
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ {
+ return Task.FromResult(new User
+ {
+ Id = lookup,
+ Username = $"User {lookup}"
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 2ee52c35aa..92eb7ac713 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -141,7 +141,6 @@ namespace osu.Game.Tournament
///
/// Add missing player info based on user IDs.
///
- ///
private bool addPlayers()
{
bool addedInfo = false;
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 9847ea020a..769b33009a 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps
///
/// Returns statistics for the contained in this beatmap.
///
- ///
IEnumerable GetStatistics();
///
diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs
index f8c9bdeaf8..86e84b0732 100644
--- a/osu.Game/Configuration/SettingsStore.cs
+++ b/osu.Game/Configuration/SettingsStore.cs
@@ -22,7 +22,6 @@ namespace osu.Game.Configuration
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 0e9382279a..67cee883c8 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds
/// such that the smaller triangles appear on top.
///
///
- ///
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
}
}
diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index ba188963ea..c8d5ce39a6 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization
///
/// Creates the default that should be used for all s.
///
- ///
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
index b25b00eb84..9d0cfedc03 100644
--- a/osu.Game/Input/KeyBindingStore.cs
+++ b/osu.Game/Input/KeyBindingStore.cs
@@ -85,7 +85,6 @@ namespace osu.Game.Input
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 6da9f12b50..d95b246c96 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -574,7 +574,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Calculate the position to be used for sample playback at a specified X position (0..1).
///
/// The lookup X position. Generally should be .
- ///
protected double CalculateSamplePlaybackBalance(double position)
{
const float balance_adjust_amount = 0.4f;
diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs
index 61f5f94142..e64298f98d 100644
--- a/osu.Game/Rulesets/Objects/SliderPath.cs
+++ b/osu.Game/Rulesets/Objects/SliderPath.cs
@@ -147,7 +147,6 @@ namespace osu.Game.Rulesets.Objects
/// to 1 (end of the path).
///
/// Ranges from 0 (beginning of the path) to 1 (end of the path).
- ///
public Vector2 PositionAt(double progress)
{
ensureValid();
@@ -161,7 +160,6 @@ namespace osu.Game.Rulesets.Objects
/// The first point has a PathType which all other points inherit.
///
/// One of the control points in the segment.
- ///
public List PointsInSegment(PathControlPoint controlPoint)
{
bool found = false;
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 38d30a2e31..efc8b50e3c 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -146,7 +146,6 @@ namespace osu.Game.Rulesets
/// The beatmap to create the hit renderer for.
/// The s to apply.
/// Unable to successfully load the beatmap to be usable with this ruleset.
- ///
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null);
///
diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs
index 018b50bd3d..410614de07 100644
--- a/osu.Game/Rulesets/Scoring/HitWindows.cs
+++ b/osu.Game/Rulesets/Scoring/HitWindows.cs
@@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring
///
/// Retrieves a mapping of s to their timing windows for all allowed s.
///
- ///
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
{
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index ef11c19e3f..222f69b025 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
+using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -55,9 +56,10 @@ namespace osu.Game.Scoring
[JsonIgnore]
public virtual RulesetInfo Ruleset { get; set; }
+ private APIMod[] localAPIMods;
private Mod[] mods;
- [JsonProperty("mods")]
+ [JsonIgnore]
[NotMapped]
public Mod[] Mods
{
@@ -66,43 +68,50 @@ namespace osu.Game.Scoring
if (mods != null)
return mods;
- if (modsJson == null)
+ if (localAPIMods == null)
return Array.Empty();
- return getModsFromRuleset(JsonConvert.DeserializeObject(modsJson));
+ var rulesetInstance = Ruleset.CreateInstance();
+ return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
set
{
- modsJson = null;
+ localAPIMods = null;
mods = value;
}
}
- private Mod[] getModsFromRuleset(DeserializedMod[] mods) => Ruleset.CreateInstance().GetAllMods().Where(mod => mods.Any(d => d.Acronym == mod.Acronym)).ToArray();
+ // Used for API serialisation/deserialisation.
+ [JsonProperty("mods")]
+ [NotMapped]
+ private APIMod[] apiMods
+ {
+ get
+ {
+ if (localAPIMods != null)
+ return localAPIMods;
- private string modsJson;
+ if (mods == null)
+ return Array.Empty();
+ return localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
+ }
+ set
+ {
+ localAPIMods = value;
+
+ // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
+ mods = null;
+ }
+ }
+
+ // Used for database serialisation/deserialisation.
[JsonIgnore]
[Column("Mods")]
public string ModsJson
{
- get
- {
- if (modsJson != null)
- return modsJson;
-
- if (mods == null)
- return null;
-
- return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
- }
- set
- {
- modsJson = value;
-
- // we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
- mods = null;
- }
+ get => JsonConvert.SerializeObject(apiMods);
+ set => apiMods = JsonConvert.DeserializeObject(value);
}
[NotMapped]
@@ -251,14 +260,6 @@ namespace osu.Game.Scoring
}
}
- [Serializable]
- protected class DeserializedMod : IMod
- {
- public string Acronym { get; set; }
-
- public bool Equals(IMod other) => Acronym == other?.Acronym;
- }
-
public override string ToString() => $"{User} playing {Beatmap}";
public bool Equals(ScoreInfo other)
diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
index a7b0fb05e3..dcf5f8a788 100644
--- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
+++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
@@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
///
/// Whether this is selected.
///
- ///
public readonly BindableBool Selected;
///
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs
new file mode 100644
index 0000000000..1b9e2bda2d
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Timing;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard
+ {
+ public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
+ : base(scoreProcessor, userIds)
+ {
+ }
+
+ public void AddClock(int userId, IClock clock)
+ {
+ if (!UserScores.TryGetValue(userId, out var data))
+ return;
+
+ ((SpectatingTrackedUserData)data).Clock = clock;
+ }
+
+ public void RemoveClock(int userId)
+ {
+ if (!UserScores.TryGetValue(userId, out var data))
+ return;
+
+ ((SpectatingTrackedUserData)data).Clock = null;
+ }
+
+ protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor);
+
+ protected override void Update()
+ {
+ base.Update();
+
+ foreach (var (_, data) in UserScores)
+ data.UpdateScore();
+ }
+
+ private class SpectatingTrackedUserData : TrackedUserData
+ {
+ [CanBeNull]
+ public IClock Clock;
+
+ public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor)
+ : base(userId, scoreProcessor)
+ {
+ }
+
+ public override void UpdateScore()
+ {
+ if (Frames.Count == 0)
+ return;
+
+ if (Clock == null)
+ return;
+
+ int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime));
+ if (frameIndex < 0)
+ frameIndex = ~frameIndex;
+ frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1);
+
+ SetFrame(Frames[frameIndex]);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index a3d27c4e71..70de067784 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -19,9 +19,7 @@ namespace osu.Game.Screens.Play.HUD
[LongRunningLoad]
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
{
- private readonly ScoreProcessor scoreProcessor;
-
- private readonly Dictionary userScores = new Dictionary();
+ protected readonly Dictionary UserScores = new Dictionary();
[Resolved]
private SpectatorStreamingClient streamingClient { get; set; }
@@ -32,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private UserLookupCache userLookupCache { get; set; }
- private Bindable scoringMode;
-
+ private readonly ScoreProcessor scoreProcessor;
private readonly BindableList playingUsers;
+ private Bindable scoringMode;
///
/// Construct a new leaderboard.
@@ -53,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
+ scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
+
foreach (var userId in playingUsers)
{
streamingClient.WatchUser(userId);
@@ -60,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD
// probably won't be required in the final implementation.
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
- var trackedUser = new TrackedUserData();
+ var trackedUser = CreateUserData(userId, scoreProcessor);
+ trackedUser.ScoringMode.BindTo(scoringMode);
- userScores[userId] = trackedUser;
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id);
+ leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
+ leaderboardScore.TotalScore.BindTo(trackedUser.Score);
+ leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
+ leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
- ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
- ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
- ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
- ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit);
+ UserScores[userId] = trackedUser;
}
-
- scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
- scoringMode.BindValueChanged(updateAllScores, true);
}
protected override void LoadComplete()
@@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD
{
streamingClient.StopWatchingUser(userId);
- if (userScores.TryGetValue(userId, out var trackedData))
+ if (UserScores.TryGetValue(userId, out var trackedData))
trackedData.MarkUserQuit();
}
@@ -110,20 +108,16 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private void updateAllScores(ValueChangedEvent mode)
+ private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() =>
{
- foreach (var trackedData in userScores.Values)
- trackedData.UpdateScore(scoreProcessor, mode.NewValue);
- }
+ if (!UserScores.TryGetValue(userId, out var trackedData))
+ return;
- private void handleIncomingFrames(int userId, FrameDataBundle bundle)
- {
- if (userScores.TryGetValue(userId, out var trackedData))
- {
- trackedData.LastHeader = bundle.Header;
- trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
- }
- }
+ trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
+ trackedData.UpdateScore();
+ });
+
+ protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor);
protected override void Dispose(bool isDisposing)
{
@@ -140,38 +134,65 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private class TrackedUserData
+ protected class TrackedUserData
{
- public IBindableNumber Score => score;
+ public readonly int UserId;
+ public readonly ScoreProcessor ScoreProcessor;
- private readonly BindableDouble score = new BindableDouble();
+ public readonly BindableDouble Score = new BindableDouble();
+ public readonly BindableDouble Accuracy = new BindableDouble(1);
+ public readonly BindableInt CurrentCombo = new BindableInt();
+ public readonly BindableBool UserQuit = new BindableBool();
- public IBindableNumber Accuracy => accuracy;
+ public readonly IBindable ScoringMode = new Bindable();
- private readonly BindableDouble accuracy = new BindableDouble(1);
+ public readonly List Frames = new List();
- public IBindableNumber CurrentCombo => currentCombo;
-
- private readonly BindableInt currentCombo = new BindableInt();
-
- public IBindable UserQuit => userQuit;
-
- private readonly BindableBool userQuit = new BindableBool();
-
- [CanBeNull]
- public FrameHeader LastHeader;
-
- public void MarkUserQuit() => userQuit.Value = true;
-
- public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
+ public TrackedUserData(int userId, ScoreProcessor scoreProcessor)
{
- if (LastHeader == null)
+ UserId = userId;
+ ScoreProcessor = scoreProcessor;
+
+ ScoringMode.BindValueChanged(_ => UpdateScore());
+ }
+
+ public void MarkUserQuit() => UserQuit.Value = true;
+
+ public virtual void UpdateScore()
+ {
+ if (Frames.Count == 0)
return;
- score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics);
- accuracy.Value = LastHeader.Accuracy;
- currentCombo.Value = LastHeader.Combo;
+ SetFrame(Frames.Last());
}
+
+ protected void SetFrame(TimedFrame frame)
+ {
+ var header = frame.Header;
+
+ Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics);
+ Accuracy.Value = header.Accuracy;
+ CurrentCombo.Value = header.Combo;
+ }
+ }
+
+ protected class TimedFrame : IComparable
+ {
+ public readonly double Time;
+ public readonly FrameHeader Header;
+
+ public TimedFrame(double time)
+ {
+ Time = time;
+ }
+
+ public TimedFrame(double time, FrameHeader header)
+ {
+ Time = time;
+ Header = header;
+ }
+
+ public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
}
}
}
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 77b3d8fc3b..441c9e048a 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking
///
/// Enumerates all s contained in this .
///
- ///
public IEnumerable GetScorePanels() => flow.Select(t => t.Panel);
///
diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
index fcf20a2eb2..5ef2458919 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps
///
/// Creates the applicable to this .
///
- ///
protected abstract Ruleset CreateRuleset();
private class ConvertResult
diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
index 76f97db59f..54a83f4305 100644
--- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps
///
/// Creates the whose legacy mod conversion is to be tested.
///
- ///
protected abstract Ruleset CreateRuleset();
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs
index 9f8a1c2e62..fdb7623be5 100644
--- a/osu.Game/Utils/Optional.cs
+++ b/osu.Game/Utils/Optional.cs
@@ -37,7 +37,6 @@ namespace osu.Game.Utils
/// Shortcase for: optional.HasValue ? optional.Value : fallback.
///
/// The fallback value to return if is false.
- ///
public T GetOr(T fallback) => HasValue ? Value : fallback;
public static implicit operator Optional(T value) => new Optional(value);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 71a6f0e5cd..92e05cb4a6 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -30,7 +30,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a389cc13dd..11124730c9 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+