diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 5a7a01df1b..32a8ba51a3 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -15,6 +15,7 @@ using osu.Framework.Threading; using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; @@ -47,6 +48,9 @@ namespace osu.Desktop [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; + [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -117,7 +121,9 @@ namespace osu.Desktop status.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); + multiplayerClient.RoomUpdated += onRoomUpdated; + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; } private void onReady(object _, ReadyMessage __) @@ -133,6 +139,8 @@ namespace osu.Desktop private void onRoomUpdated() => schedulePresenceUpdate(); + private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate(); + private ScheduledDelegate? presenceUpdateDelegate; private void schedulePresenceUpdate() @@ -167,7 +175,7 @@ namespace osu.Desktop presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) + if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { @@ -229,10 +237,8 @@ namespace osu.Desktop presence.Assets.LargeImageText = string.Empty; else { - if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) - presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); - else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); + var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value); + presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); } // small image @@ -327,25 +333,14 @@ namespace osu.Desktop return true; } - private static int? getBeatmapID(UserActivity activity) - { - switch (activity) - { - case UserActivity.InGame game: - return game.BeatmapID; - - case UserActivity.EditingBeatmap edit: - return edit.BeatmapID; - } - - return null; - } - protected override void Dispose(bool isDisposing) { if (multiplayerClient.IsNotNull()) multiplayerClient.RoomUpdated -= onRoomUpdated; + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + client.Dispose(); base.Dispose(isDisposing); } diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs index aa229c7d06..4d3023e92e 100644 --- a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs +++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs @@ -4,28 +4,54 @@ using System.Collections.Generic; using BenchmarkDotNet.Attributes; using osu.Framework.Utils; -using osu.Game.Rulesets.Objects; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Benchmarks { public class BenchmarkUnstableRate : BenchmarkTest { - private List events = null!; + private readonly List> incrementalEventLists = new List>(); public override void SetUp() { base.SetUp(); - events = new List(); - for (int i = 0; i < 1000; i++) - events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); + var events = new List(); + + for (int i = 0; i < 2048; i++) + { + // Ensure the object has hit windows populated. + var hitObject = new HitCircle(); + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null)); + + incrementalEventLists.Add(new List(events)); + } } [Benchmark] public void CalculateUnstableRate() { - _ = events.CalculateUnstableRate(); + for (int i = 0; i < 2048; i++) + { + var events = incrementalEventLists[i]; + _ = events.CalculateUnstableRate(); + } + } + + [Benchmark] + public void CalculateUnstableRateUsingIncrementalCalculation() + { + HitEventExtensions.UnstableRateCalculationResult? last = null; + + for (int i = 0; i < 2048; i++) + { + var events = incrementalEventLists[i]; + last = events.CalculateUnstableRate(last); + } } } } diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index f975c7f1d4..d9cc224ad1 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40); + SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration if (Get(ManiaRulesetSetting.ScrollTime) is double scrollTime) { - SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); + SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); SetValue(ManiaRulesetSetting.ScrollTime, null); } #pragma warning restore CS0618 @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { - new TrackedSetting(ManiaRulesetSetting.ScrollSpeed, + new TrackedSetting(ManiaRulesetSetting.ScrollSpeed, speed => new SettingDescription( rawValue: speed, name: RulesetSettingsStrings.ScrollSpeed, diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 4c4cf519ce..181bc7341c 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override void Update() { - TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; + TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; base.Update(); } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 30eca0636c..17add32513 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.ScrollingDirection, Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, - new SettingsSlider + new SettingsSlider { LabelText = RulesetSettingsStrings.ScrollSpeed, - Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed), - KeyboardStep = 5 + Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed), + KeyboardStep = 1 }, new SettingsCheckbox { @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania }; } - private partial class ManiaScrollSlider : RoundedSliderBar + private partial class ManiaScrollSlider : RoundedSliderBar { public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value); } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index cb42b2b62a..8f425edc44 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private Drawable getResult(HitResult result) { - if (!hit_result_mapping.ContainsKey(result)) + if (!hit_result_mapping.TryGetValue(result, out var value)) return null; - string filename = this.GetManiaSkinConfig(hit_result_mapping[result])?.Value + string filename = this.GetManiaSkinConfig(value)?.Value ?? default_hit_result_skin_filenames[result]; var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index aed53e157a..d173ae4143 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); - private readonly BindableInt configScrollSpeed = new BindableInt(); + private readonly BindableDouble configScrollSpeed = new BindableDouble(); private double currentTimeRange; protected double TargetTimeRange; @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The scroll speed. /// The scroll time. - public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; + public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 768a764ad1..2fe0d51034 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.X, + Precision = 0.01f, }; /// @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.Y, + Precision = 0.01f, }; /// @@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 4f, MaxValue = 128f, + Precision = 0.01f, }; /// @@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = -180f, MaxValue = 180f, + Precision = 0.01f, }; /// diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index d4a0f243e4..5d09267c21 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Texture = source.GetTexture("spinner-top"), }, - fixedMiddle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle"), - }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle2"), }, + fixedMiddle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle"), + }, } }); diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 54ebebeb7b..b5c299ed9d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1000,7 +1000,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); - Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); + Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 27b8d3f21e..b1e2730703 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -64,6 +65,10 @@ namespace osu.Game.Tests // Beatmap must be imported before the collection manager is loaded. if (withBeatmap) BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); + + // the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase. + // the ruleset bindable is not meant to be nullable, so assign any ruleset in here. + Ruleset.Value = RulesetStore.AvailableRulesets.First(); } } } diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 5a416d05d7..03dc91b5d4 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,12 +20,53 @@ namespace osu.Game.Tests.NonVisual.Ranking public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); var unstableRate = new UnstableRate(events); Assert.IsNotNull(unstableRate.Value); - Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); + Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON); + } + + [Test] + public void TestDistributedHitsIncrementalRewind() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); + + HitEventExtensions.UnstableRateCalculationResult result = null; + + for (int i = 0; i < events.Count; i++) + { + result = events.GetRange(0, i + 1) + .CalculateUnstableRate(result); + } + + result = events.GetRange(0, 2).CalculateUnstableRate(result); + + Assert.IsNotNull(result!.Result); + Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON); + } + + [Test] + public void TestDistributedHitsIncremental() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); + + HitEventExtensions.UnstableRateCalculationResult result = null; + + for (int i = 0; i < events.Count; i++) + { + result = events.GetRange(0, i + 1) + .CalculateUnstableRate(result); + } + + Assert.IsNotNull(result!.Result); + Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 5cc1e64197..765fe1ecf6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL); - void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); - void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); + void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", + () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); + + void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", + () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); } [Test] @@ -781,15 +784,39 @@ namespace osu.Game.Tests.Visual.Editing setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); dismissPopover(); - hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); - hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); - hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + assertNoChanges(); - AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); + AddStep("select first object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]); + }); + assertNoChanges(); - hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); - hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); - hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + AddStep("select second object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]); + }); + assertNoChanges(); + + AddStep("select first object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]); + }); + assertNoChanges(); + + void assertNoChanges() + { + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT); + } } private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => @@ -883,11 +910,12 @@ namespace osu.Game.Tests.Visual.Editing return h.Samples.All(o => o.Volume == volume); }); - private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); - }); + private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); + }); private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => { @@ -944,29 +972,33 @@ namespace osu.Game.Tests.Visual.Editing return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); }); - private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); - }); + private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); + }); - private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", + () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); + }); - private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); - private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 460d7814e0..609bc6e166 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osu.Game.Overlays.Login; using osu.Game.Overlays.Settings; +using osu.Game.Tests.Visual.Online; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK.Input; @@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus [Resolved] private OsuConfigManager configManager { get; set; } = null!; + [Cached(typeof(LocalUserStatisticsProvider))] + private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider(); + [BackgroundDependencyLoader] private void load() { @@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088"); assertAPIState(APIState.Online); + AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); AddStep("click on flag", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 29fa7287d2..0d981014b8 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -3,8 +3,10 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus ((StarFountain)Children[1]).Shoot(-1); }); } + + [Test] + public void TestGameplayStarFountainsSetting() + { + Bindable starFountainsEnabled = null!; + + AddStep("load configuration", () => + { + var config = new OsuConfigManager(LocalStorage); + starFountainsEnabled = config.GetBindable(OsuSetting.StarFountains); + }); + + AddStep("make fountains", () => + { + Children = new Drawable[] + { + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 75, + }, + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -75, + }, + }; + }); + + AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true); + AddRepeatStep("activate fountains (enabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + + AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false); + AddRepeatStep("attempt to activate fountains (disabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + + AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true); + AddRepeatStep("activate fountains (re-enabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index 71a45e2398..1af4af8f6b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Gain", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Loss", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Tiny increase in PP", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("No change 1", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Was null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Became null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index eda7ce925a..5646649d33 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("retry count is 1", () => player.RestartCount == 1); } + [Test] + public void TestLastScoreNullAfterExitingPlayer() + { + AddUntilStep("wait for last play null", getLastPlay, () => Is.Null); + + var getOriginalPlayer = playToCompletion(); + + AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo)); + + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit()); + AddUntilStep("wait for last play null", getLastPlay, () => Is.Null); + + ScoreInfo getLastPlay() => Game.Dependencies.Get().Get(Static.LastLocalUserScore); + } + [Test] public void TestRetryImmediatelyAfterCompletion() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 3d6fe50d34..ab9ee1d8cc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online waitForChannel1Visible(); } + [Test] + public void TestPublicChannelsSortedByName() + { + // Intentionally join back to front. + AddStep("Show overlay with channel 2", () => + { + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2); + chatOverlay.Show(); + }); + AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2); + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1); + + AddStep("message in channel 2", () => + { + testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1); + + ChannelListItem getFirstVisiblePublicChannel() => + chatOverlay.ChildrenOfType().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType().First(item => item.Channel.Type == ChannelType.Public); + } + + [Test] + public void TestPrivateChannelsSortedByRecent() + { + Channel pmChannel1 = createPrivateChannel(); + Channel pmChannel2 = createPrivateChannel(); + + joinChannel(pmChannel1); + joinChannel(pmChannel2); + + AddStep("Show overlay", () => chatOverlay.Show()); + + AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1); + + AddStep("message in channel 2", () => + { + pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + + AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2); + + AddStep("message in channel 1", () => + { + pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + + AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1); + + ChannelListItem getFirstVisiblePMChannel() => + chatOverlay.ChildrenOfType().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType().First(item => item.Channel.Type == ChannelType.PM); + } + [Test] public void TestKeyboardNewChannel() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..f24a9333c1 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -0,0 +1,179 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene + { + private LocalUserStatisticsProvider statisticsProvider = null!; + + private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("clear statistics", () => serverSideStatistics.Clear()); + + setUser(1000); + + AddStep("setup provider", () => + { + OsuTextFlowContainer text; + + ((DummyAPIAccess)API).HandleRequest = r => + { + switch (r) + { + case GetUserRequest userRequest: + int userId = int.Parse(userRequest.Lookup); + string rulesetName = userRequest.Ruleset!.ShortName; + var response = new APIUser + { + Id = userId, + Statistics = tryGetStatistics(userId, rulesetName) + }; + + userRequest.TriggerSuccess(response); + return true; + + default: + return false; + } + }; + + Clear(); + Add(statisticsProvider = new LocalUserStatisticsProvider()); + Add(text = new OsuTextFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + statisticsProvider.StatisticsUpdated += update => + { + text.Clear(); + + foreach (var ruleset in Dependencies.Get().AvailableRulesets) + { + text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics + ? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})" + : $"{ruleset.Name} statistics: (null)"); + text.NewLine(); + } + + text.AddText($"latest update: {update.Ruleset}" + + $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})"); + }; + + Ruleset.Value = new OsuRuleset().RulesetInfo; + }); + } + + [Test] + public void TestInitialStatistics() + { + AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000)); + AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000)); + AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000)); + AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000)); + } + + [Test] + public void TestUserChanges() + { + setUser(1001); + + AddStep("update statistics for user 1000", () => + { + serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 }; + serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 }; + }); + + AddAssert("statistics matches user 1001 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); + + AddAssert("statistics matches user 1001 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(3_000_000)); + + setUser(1000, false); + + AddAssert("statistics matches user 1000 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(5_000_000)); + + AddAssert("statistics matches user 1000 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(6_000_000)); + } + + [Test] + public void TestRefetchStatistics() + { + UserStatisticsUpdate? update = null; + + setUser(1001); + + AddStep("update statistics server side", + () => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 }); + + AddAssert("statistics match old score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); + + AddStep("setup event", () => + { + update = null; + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; + }); + + AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo)); + AddUntilStep("statistics update raised", + () => update?.NewStatistics.TotalScore, + () => Is.EqualTo(9_000_000)); + AddAssert("statistics match new score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(9_000_000)); + + void onStatisticsUpdated(UserStatisticsUpdate u) => update = u; + } + + private UserStatistics tryGetStatistics(int userId, string rulesetName) + => serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics(); + + private void setUser(int userId, bool generateStatistics = true) + { + AddStep($"set local user to {userId}", () => + { + if (generateStatistics) + { + serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 }; + serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 }; + serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 }; + serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 }; + } + + ((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId }; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index ab3d5907ef..3f1d961588 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneUserPanel : OsuTestScene { - private readonly Bindable activity = new Bindable(); + private readonly Bindable activity = new Bindable(); private readonly Bindable status = new Bindable(); - private UserGridPanel boundPanel1; - private TestUserListPanel boundPanel2; + private UserGridPanel boundPanel1 = null!; + private TestUserListPanel boundPanel2 = null!; [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Cached(typeof(LocalUserStatisticsProvider))] + private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); [Resolved] - private IRulesetStore rulesetStore { get; set; } + private IRulesetStore rulesetStore { get; set; } = null!; [SetUp] public void SetUp() => Schedule(() => @@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online activity.Value = null; status.Value = null; - Child = new FillFlowContainer + Remove(statisticsProvider, false); + Clear(); + Add(statisticsProvider); + + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } }) { Width = 300 } } - }; + }); boundPanel1.Status.BindTo(status); boundPanel1.Activity.BindTo(activity); @@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online { AddStep("update statistics", () => { - API.UpdateStatistics(new UserStatistics + statisticsProvider.UpdateStatistics(new UserStatistics { GlobalRank = RNG.Next(100000), CountryRank = RNG.Next(100000) - }); + }, Ruleset.Value); }); AddStep("set statistics to something big", () => { - API.UpdateStatistics(new UserStatistics + statisticsProvider.UpdateStatistics(new UserStatistics { GlobalRank = RNG.Next(1_000_000, 100_000_000), CountryRank = RNG.Next(1_000_000, 100_000_000) - }); - }); - AddStep("set statistics to empty", () => - { - API.UpdateStatistics(new UserStatistics()); + }, Ruleset.Value); }); + AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); @@ -201,5 +204,11 @@ namespace osu.Game.Tests.Visual.Online public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; } + + public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider + { + public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action? callback = null) + => base.UpdateStatistics(newStatistics, ruleset, callback); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 006610dccd..d16ed46bd2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -58,6 +59,16 @@ namespace osu.Game.Tests.Visual.Online return true; } + if (req is GetUserBeatmapsRequest getUserBeatmapsRequest) + { + getUserBeatmapsRequest.TriggerSuccess(new List + { + CreateAPIBeatmapSet(), + CreateAPIBeatmapSet() + }); + return true; + } + return false; }; }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs index e7ad07041c..d410b7f3a4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => false; + private LocalUserStatisticsProvider statisticsProvider = null!; private UserStatisticsWatcher watcher = null!; [Resolved] @@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("create watcher", () => { - Child = watcher = new UserStatisticsWatcher(); + Clear(); + Add(statisticsProvider = new LocalUserStatisticsProvider()); + Add(watcher = new UserStatisticsWatcher(statisticsProvider)); }); } @@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -146,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online // note ordering - in this test processing completes *before* the registration is added. feignScoreProcessing(userId, ruleset, 5_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); @@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online feignScoreProcessing(userId, ruleset, 6_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); @@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddUntilStep("update received", () => update != null); - AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); - AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000)); } private int nextUserId = 2000; @@ -289,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online }); } - private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => + private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => AddStep("register for updates", () => { watcher.RegisterForStatisticsUpdateAfter( diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 5977e67b0e..33bd573617 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Graphics.Containers; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private const int real_user_position = 200; - private TestResultsScreen resultsScreen = null!; + private ResultsScreen resultsScreen = null!; private int lowestScoreId; // Score ID of the lowest score in the list. private int highestScoreId; // Score ID of the highest score in the list. @@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowWithUserScore() + public void TestShowUserScore() { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); @@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowNullUserScore() + public void TestShowUserBest() + { + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createUserBestResults(); + waitForDisplay(); + + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded); + AddAssert($"score panel position is {real_user_position}", + () => this.ChildrenOfType().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position); + } + + [Test] + public void TestShowNonUserScores() { AddStep("bind user score info handler", () => bindHandler()); - createResults(); + createUserBestResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); @@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind user score info handler", () => bindHandler(true, userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); @@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowNullUserScoreWithDelay() + public void TestShowNonUserScoresWithDelay() { AddStep("bind delayed handler", () => bindHandler(true)); - createResults(); + createUserBestResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); @@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind delayed handler", () => bindHandler(true)); - createResults(); + createUserBestResults(); waitForDisplay(); for (int i = 0; i < 2; i++) @@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); } } @@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); - createResults(); + createUserBestResults(); waitForDisplay(); int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); + AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); } @@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false)); + AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); - AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); + AddAssert("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } } + /// + /// Shows the with no scores provided by the API. + /// [Test] - public void TestShowWithNoScores() + public void TestShowUserBestWithNoScoresPresent() { AddStep("bind user score info handler", () => bindHandler(noScores: true)); - createResults(); - AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); + createUserBestResults(); + AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } - private void createResults(Func? getScore = null) + private void createResultsWithScore(Func getScore) { AddStep("load results", () => { - LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); @@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); } + private void createUserBestResults() + { + AddStep("load results", () => + { + LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + }, 2)); + }); + + AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); + } + private void waitForDisplay() { AddUntilStep("wait for scores loaded", () => requestComplete // request handler may need to fire more than once to get scores. && totalCount > 0 - && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount - && resultsScreen.ScorePanelList.AllPanelsVisible); + && resultsScreen.ChildrenOfType().Single().GetScorePanels().Count() == totalCount + && resultsScreen.ChildrenOfType().Single().AllPanelsVisible); AddWaitStep("wait for display", 5); } @@ -231,6 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists // pre-check for requests we should be handling (as they are scheduled below). switch (request) { + case ShowPlaylistScoreRequest: case ShowPlaylistUserScoreRequest: case IndexPlaylistScoresRequest: break; @@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists switch (request) { - case ShowPlaylistUserScoreRequest s: + case ShowPlaylistScoreRequest s: if (userScore == null) triggerFail(s); else @@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists break; + case ShowPlaylistUserScoreRequest u: + if (userScore == null) + triggerFail(u); + else + triggerSuccess(u, createUserResponse(userScore)); + + break; + case IndexPlaylistScoresRequest i: triggerSuccess(i, createIndexResponse(i, noScores)); break; @@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = userScore.MaxCombo, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = userScore.MaxCombo, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = 1000, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -410,18 +462,22 @@ namespace osu.Game.Tests.Visual.Playlists }; } - private partial class TestResultsScreen : PlaylistItemUserResultsScreen + private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen { - public new LoadingSpinner LeftSpinner => base.LeftSpinner; - public new LoadingSpinner CentreSpinner => base.CentreSpinner; - public new LoadingSpinner RightSpinner => base.RightSpinner; - public new ScorePanelList ScorePanelList => base.ScorePanelList; - - public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem) + public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) { AllowRetry = true; } } + + private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen + { + public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId) + : base(roomId, playlistItem, userId) + { + AllowRetry = true; + } + } } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index ffc7d88a34..b406ea369f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking }); private void displayUpdate(UserStatistics before, UserStatistics after) => - AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); + AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f46f76cbb8..c12b9d29bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking UserStatisticsWatcher userStatisticsWatcher = null!; ScoreInfo score = null!; - AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); + AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider()))); AddStep("set user statistics update", () => { score = TestResources.CreateTestScoreInfo(); score.OnlineID = 1234; - ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, + ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking Score = { Value = score }, DisplayedUserStatisticsUpdate = { - Value = new UserStatisticsUpdate(score, new UserStatistics + Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 66862e1b78..bd5c43d242 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -8,14 +8,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneBeatmapRecommendations : OsuGameTestScene { - [Resolved] - private IRulesetStore rulesetStore { get; set; } - [SetUpSteps] public override void SetUpSteps() { AddStep("populate ruleset statistics", () => { - Dictionary rulesetStatistics = new Dictionary(); - - rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => + ((DummyAPIAccess)API).HandleRequest = r => { - rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics + switch (r) { - PP = getNecessaryPP(rulesetInfo.OnlineID) - }; - }); + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Id = 99, + Statistics = new UserStatistics + { + PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0) + } + }); - API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; + return true; + + default: + return false; + } + }; }); decimal getNecessaryPP(int? rulesetID) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index f1463eb632..d94c09d40f 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -150,7 +150,7 @@ namespace osu.Game.Beatmaps public bool EpilepsyWarning { get; set; } - public bool SamplesMatchPlaybackRate { get; set; } = true; + public bool SamplesMatchPlaybackRate { get; set; } /// /// The time at which this beatmap was last played by the local user. @@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps public double? EditorTimestamp { get; set; } [Ignored] - public CountdownType Countdown { get; set; } = CountdownType.Normal; + public CountdownType Countdown { get; set; } = CountdownType.None; /// /// The number of beats to move the countdown backwards (compared to its default location). diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4191771116..f1ce977d96 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0; - if (refetch || beatmapInfo.IsManaged || missingFiles) + if (beatmapInfo.IsManaged) + { + beatmapInfo = beatmapInfo.Detach(); + } + else if (refetch || missingFiles) { Guid id = beatmapInfo.ID; beatmapInfo = Realm.Run(r => r.Find(id)?.Detach()) ?? beatmapInfo; diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index ec00756fd9..d132b86052 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -9,9 +9,11 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Game.Online.API; +using osu.Game.Online; using osu.Game.Rulesets; +using osu.Game.Users; namespace osu.Game.Beatmaps { @@ -21,18 +23,63 @@ namespace osu.Game.Beatmaps /// public partial class DifficultyRecommender : Component { - [Resolved] - private IAPIProvider api { get; set; } + private readonly LocalUserStatisticsProvider statisticsProvider; [Resolved] - private Bindable ruleset { get; set; } + private Bindable gameRuleset { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); + /// + /// Rulesets ordered descending by their respective recommended difficulties. + /// The currently selected ruleset will always be first. + /// + private IEnumerable orderedRulesets + { + get + { + if (LoadState < LoadState.Ready || gameRuleset.Value == null) + return Enumerable.Empty(); + + return recommendedDifficultyMapping + .OrderByDescending(pair => pair.Value) + .Select(pair => pair.Key) + .Where(r => !r.Equals(gameRuleset.Value.ShortName, StringComparison.Ordinal)) + .Prepend(gameRuleset.Value.ShortName); + } + } + + public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider) + { + this.statisticsProvider = statisticsProvider; + } + [BackgroundDependencyLoader] private void load() { - api.LocalUser.BindValueChanged(_ => populateValues(), true); + foreach (var ruleset in rulesets.AvailableRulesets) + { + if (statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics) + updateMapping(ruleset, statistics); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; + } + + private void onStatisticsUpdated(UserStatisticsUpdate update) => updateMapping(update.Ruleset, update.NewStatistics); + + private void updateMapping(RulesetInfo ruleset, UserStatistics statistics) + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195; } /// @@ -64,35 +111,12 @@ namespace osu.Game.Beatmaps return null; } - private void populateValues() + protected override void Dispose(bool isDisposing) { - if (api.LocalUser.Value.RulesetsStatistics == null) - return; + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; - foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) - { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195; - } - } - - /// - /// Rulesets ordered descending by their respective recommended difficulties. - /// The currently selected ruleset will always be first. - /// - private IEnumerable orderedRulesets - { - get - { - if (LoadState < LoadState.Ready || ruleset.Value == null) - return Enumerable.Empty(); - - return recommendedDifficultyMapping - .OrderByDescending(pair => pair.Value) - .Select(pair => pair.Key) - .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal)) - .Prepend(ruleset.Value.ShortName); - } + base.Dispose(isDisposing); } } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index f18355505a..599d1b380a 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables }; Status = BeatmapOnlineStatus.None; - TextPadding = new MarginPadding { Horizontal = 5, Bottom = 1 }; + TextPadding = new MarginPadding { Horizontal = 4, Bottom = 1 }; } protected override void LoadComplete() diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 25e42bcbf7..56103c1d6d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -20,9 +20,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu { public const float TRANSITION_DURATION = 340; - public const float CORNER_RADIUS = 10; + public const float CORNER_RADIUS = 8; - protected const float WIDTH = 430; + protected const float WIDTH = 345; public IBindable Expanded { get; } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 2c2761ff0c..ebd0113379 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; - private const float height = 140; + private const float height = 112; [Cached] private readonly BeatmapCardContent content; @@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { - Margin = new MarginPadding(5), + Margin = new MarginPadding(4), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(1) @@ -80,7 +80,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = WIDTH - height + CORNER_RADIUS, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, - ButtonsExpandedWidth = 30, + ButtonsExpandedWidth = 24, Children = new Drawable[] { new FillFlowContainer @@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, titleBadgeArea = new FillFlowContainer @@ -142,7 +142,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, Empty() @@ -154,7 +154,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, Text = BeatmapSet.Source, Shadow = false, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold), Colour = colourProvider.Content2 }, } @@ -173,18 +173,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), + Spacing = new Vector2(0, 2), AlwaysPresent = true, Children = new Drawable[] { new LinkFlowContainer(s => { s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); }).With(d => { d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; + d.Margin = new MarginPadding { Top = 1 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(BeatmapSet.Author); }), @@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadProgressBar = new BeatmapCardDownloadProgressBar { RelativeSizeAxes = Axes.X, - Height = 6, + Height = 5, Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { BindTarget = DownloadTracker.State }, @@ -231,17 +231,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Padding = new MarginPadding { Horizontal = 8, Vertical = 10 }, Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.FeaturedInSpotlight) { @@ -249,7 +249,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -259,7 +259,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }; } @@ -288,7 +288,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { BeatmapCardStatistic withMargin(BeatmapCardStatistic original) { - original.Margin = new MarginPadding { Right = 10 }; + original.Margin = new MarginPadding { Right = 8 }; return original; } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index 3a1b8f7e86..a11ef0f95c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, - Spacing = new Vector2(4, 0), + Spacing = new Vector2(3, 0), Children = new Drawable[] { new BeatmapSetOnlineStatusPill @@ -33,13 +33,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Both, Status = beatmapSet.Status, Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, + TextSize = 13f }, new DifficultySpectrumDisplay(beatmapSet) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - DotSize = new Vector2(6, 12) + DotSize = new Vector2(5, 10) } } }; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 46ab7ec5f6..724919f3bd 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; - public const float HEIGHT = 100; + public const float HEIGHT = 80; [Cached] private readonly BeatmapCardContent content; @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { - Margin = new MarginPadding(5), + Margin = new MarginPadding(4), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(1) @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = WIDTH - HEIGHT + CORNER_RADIUS, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, - ButtonsExpandedWidth = 30, + ButtonsExpandedWidth = 24, Children = new Drawable[] { new FillFlowContainer @@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, titleBadgeArea = new FillFlowContainer @@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, Empty() @@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards new LinkFlowContainer(s => { s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); }).With(d => { d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; + d.Margin = new MarginPadding { Top = 1 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(BeatmapSet.Author); }), @@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), + Spacing = new Vector2(0, 2), AlwaysPresent = true, Children = new Drawable[] { @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(8, 0), Alpha = 0, AlwaysPresent = true, ChildrenEnumerable = createStatistics() @@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadProgressBar = new BeatmapCardDownloadProgressBar { RelativeSizeAxes = Axes.X, - Height = 6, + Height = 5, Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { BindTarget = DownloadTracker.State }, @@ -213,17 +213,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Padding = new MarginPadding { Horizontal = 8, Vertical = 10 }, Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.FeaturedInSpotlight) { @@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -241,7 +241,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }; } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs index 6fd7142c05..ece52d0fa9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs @@ -46,21 +46,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), + Spacing = new Vector2(4, 0), Children = new Drawable[] { spriteIcon = new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(10), + Size = new Vector2(8), Margin = new MarginPadding { Top = 1 } }, spriteText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(size: 14) + Font = OsuFont.Default.With(size: 11) } } }; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3d8c8a6e7a..4d7ac355e0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -192,7 +192,6 @@ namespace osu.Game.Beatmaps.Formats private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) { beatmapInfo.WidescreenStoryboard = false; - beatmapInfo.SamplesMatchPlaybackRate = false; } protected override void ParseLine(Beatmap beatmap, Section section, string line) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 362c06849d..4f62db8cf7 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -138,6 +138,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LightenDuringBreaks, true); SetDefault(OsuSetting.HitLighting, true); + SetDefault(OsuSetting.StarFountains, true); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); @@ -214,6 +215,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorContractSidebars, false); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); + SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -413,6 +415,7 @@ namespace osu.Game.Configuration NotifyOnPrivateMessage, UIHoldActivationDelay, HitLighting, + StarFountains, MenuBackgroundSource, GameplayDisableWinKey, SeasonalBackgroundMode, @@ -444,5 +447,6 @@ namespace osu.Game.Configuration EditorRotationOrigin, EditorTimelineShowBreaks, EditorAdjustExistingObjectsOnTimingChanges, + AlwaysRequireHoldingForPause } } diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 225f209380..18631f5d00 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play; namespace osu.Game.Configuration { @@ -77,7 +78,8 @@ namespace osu.Game.Configuration TouchInputActive, /// - /// Stores the local user's last score (can be completed or aborted). + /// Contains the local user's last score (can be completed or aborted) after exiting . + /// Will be cleared to null when leaving . /// LastLocalUserScore, diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 6de61f7ebe..2715f0b8cf 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -74,6 +74,11 @@ namespace osu.Game.Localisation /// public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); + /// + /// "Star fountains" + /// + public static LocalisableString StarFountains => new TranslatableString(getKey(@"star_fountains"), @"Star fountains"); + /// /// "Always show key overlay" /// @@ -89,6 +94,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button"); + /// + /// "Require holding key to pause gameplay" + /// + public static LocalisableString AlwaysRequireHoldForMenu => new TranslatableString(getKey(@"require_holding_key_to_pause_gameplay"), @"Require holding key to pause gameplay"); + /// /// "Always play first combo break sound" /// diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs new file mode 100644 index 0000000000..f97ad5fa2c --- /dev/null +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -0,0 +1,154 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class MenuTipStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MenuTip"; + + /// + /// "Press Ctrl-T anywhere in the game to toggle the toolbar!" + /// + public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); + + /// + /// "Press Ctrl-O anywhere in the game to access settings!" + /// + public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!"); + + /// + /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" + /// + public static LocalisableString DynamicSettings => new TranslatableString(getKey(@"dynamic_settings"), @"All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"); + + /// + /// "New features are coming online every update. Make sure to stay up-to-date!" + /// + public static LocalisableString NewFeaturesAreComingOnline => new TranslatableString(getKey(@"new_features_are_coming_online"), @"New features are coming online every update. Make sure to stay up-to-date!"); + + /// + /// "If you find the UI too large or small, try adjusting UI scale in settings!" + /// + public static LocalisableString UIScalingSettings => new TranslatableString(getKey(@"ui_scaling_settings"), @"If you find the UI too large or small, try adjusting UI scale in settings!"); + + /// + /// "Try adjusting the "Screen Scaling" mode to change your gameplay or UI area, even in fullscreen!" + /// + public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!"); + + /// + /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using Ctrl-B!" + /// + public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"); + + /// + /// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!" + /// + public static LocalisableString ReplaySeeking => new TranslatableString(getKey(@"replay_seeking"), @"Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"); + + /// + /// "Try scrolling right in mod select to find a bunch of new fun mods!" + /// + public static LocalisableString TryNewMods => new TranslatableString(getKey(@"try_new_mods"), @"Try scrolling right in mod select to find a bunch of new fun mods!"); + + /// + /// "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!" + /// + public static LocalisableString EmbeddedWebContent => new TranslatableString(getKey(@"embedded_web_content"), @"Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!"); + + /// + /// "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!" + /// + public static LocalisableString BeatmapRightClick => new TranslatableString(getKey(@"beatmap_right_click"), @"Get more details, hide or delete a beatmap by right-clicking on its panel at song select!"); + + /// + /// "Check out the "playlists" system, which lets users create their own custom and permanent leaderboards!" + /// + public static LocalisableString DiscoverPlaylists => new TranslatableString(getKey(@"discover_playlists"), @"Check out the ""playlists"" system, which lets users create their own custom and permanent leaderboards!"); + + /// + /// "Toggle advanced frame / thread statistics with Ctrl-F11!" + /// + public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!"); + + /// + /// "You can pause during a replay by pressing Space!" + /// + public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!"); + + /// + /// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!" + /// + public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); + + /// + /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" + /// + public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); + + /// + /// "You can create mod presets to make toggling your favourite mod combinations easier!" + /// + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favourite mod combinations easier!"); + + /// + /// "Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!" + /// + public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"); + + /// + /// "Press Ctrl-Shift-R to switch to a random skin!" + /// + public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!"); + + /// + /// "While watching a replay, press Ctrl-H to toggle replay settings!" + /// + public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!"); + + /// + /// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!" + /// + public static LocalisableString CopyModsFromScore => new TranslatableString(getKey(@"copy_mods_from_score"), @"You can easily copy the mods from scores on a leaderboard by right-clicking on them!"); + + /// + /// "Ctrl-Enter at song select will start a beatmap in autoplay mode!" + /// + public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!"); + + /// + /// "Multithreading support means that even with low "FPS" your input and judgements will be accurate!" + /// + public static LocalisableString MultithreadingSupport => new TranslatableString(getKey(@"multithreading_support"), @"Multithreading support means that even with low ""FPS"" your input and judgements will be accurate!"); + + /// + /// "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!" + /// + public static LocalisableString TemporaryDeleteOperations => new TranslatableString(getKey(@"temporary_delete_operations"), @"All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!"); + + /// + /// "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!" + /// + public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"); + + /// + /// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!" + /// + public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"); + + /// + /// "Drag and drop any image into the skin editor to load it in quickly!" + /// + public static LocalisableString DragAndDropImageInSkinEditor => new TranslatableString(getKey(@"drag_and_drop_image_in_skin_editor"), @"Drag and drop any image into the skin editor to load it in quickly!"); + + /// + /// "a tip for you:" + /// + public static LocalisableString MenuTipTitle => new TranslatableString(getKey(@"menu_tip_title"), @"a tip for you:"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index e3d51f1124..9434cd53de 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -80,9 +80,9 @@ namespace osu.Game.Localisation public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring"); /// - /// "{0}ms (speed {1})" + /// "{0}ms (speed {1:N1})" /// - public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed); + public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed); /// /// "Touch control scheme" diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index c8992c108e..ec48fa2436 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -59,7 +59,6 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; public IBindable Activity => activity; - public IBindable Statistics => statistics; public INotificationsClient NotificationsClient { get; } @@ -74,8 +73,6 @@ namespace osu.Game.Online.API private Bindable configStatus { get; } = new Bindable(); private Bindable localUserStatus { get; } = new Bindable(); - private Bindable statistics { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -604,14 +601,6 @@ namespace osu.Game.Online.API flushQueue(); } - public void UpdateStatistics(UserStatistics newStatistics) - { - statistics.Value = newStatistics; - - if (IsLoggedIn) - localUser.Value.Statistics = newStatistics; - } - public void UpdateLocalFriends() { if (!IsLoggedIn) @@ -630,11 +619,7 @@ namespace osu.Game.Online.API private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => - { - localUser.Value = user; - statistics.Value = user.Statistics; - }, false); + private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index f0da0c25da..5d63c04925 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -30,8 +30,6 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); - public Bindable Statistics { get; } = new Bindable(); - public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -178,11 +176,6 @@ namespace osu.Game.Online.API private void onSuccessfulLogin() { state.Value = APIState.Online; - Statistics.Value = new UserStatistics - { - GlobalRank = 1, - CountryRank = 1 - }; } public void Logout() @@ -193,14 +186,6 @@ namespace osu.Game.Online.API LocalUser.Value = new GuestUser(); } - public void UpdateStatistics(UserStatistics newStatistics) - { - Statistics.Value = newStatistics; - - if (IsLoggedIn) - LocalUser.Value.Statistics = newStatistics; - } - public void UpdateLocalFriends() { } @@ -220,7 +205,6 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; - IBindable IAPIProvider.Statistics => Statistics; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 4b1aed236d..1c4b2da742 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -29,11 +29,6 @@ namespace osu.Game.Online.API /// IBindable Activity { get; } - /// - /// The current user's online statistics. - /// - IBindable Statistics { get; } - /// /// The language supplied by this provider to API requests. /// @@ -129,11 +124,6 @@ namespace osu.Game.Online.API /// void Logout(); - /// - /// Sets Statistics bindable. - /// - void UpdateStatistics(UserStatistics newStatistics); - /// /// Update the friends status of the current user. /// diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 5d80fde515..a829484506 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -223,8 +223,10 @@ namespace osu.Game.Online.API.Requests.Responses /// /// User statistics for the requested ruleset (in the case of a or response). - /// Otherwise empty. /// + /// + /// This returns null when accessed from . Use instead. + /// [JsonProperty(@"statistics")] public UserStatistics Statistics { diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 15ce926039..9de77237b4 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -161,7 +161,7 @@ namespace osu.Game.Online.Chat Messages.AddRange(messages); long? maxMessageId = messages.Max(m => m.Id); - if (maxMessageId > LastMessageId) + if (LastMessageId == null || maxMessageId > LastMessageId) LastMessageId = maxMessageId; purgeOldMessages(); diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..22d5788c87 --- /dev/null +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -0,0 +1,92 @@ +// 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.Graphics; +using osu.Game.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Online +{ + /// + /// A component that keeps track of the latest statistics for the local user. + /// + public partial class LocalUserStatisticsProvider : Component + { + /// + /// Invoked whenever a change occured to the statistics of any ruleset, + /// either due to change in local user (log out and log in) or as a result of score submission. + /// + /// + /// This does not guarantee the presence of the old statistics, + /// specifically in the case of initial population or change in local user. + /// + public event Action? StatisticsUpdated; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly Dictionary statisticsCache = new Dictionary(); + + /// + /// Returns the currently available for the given ruleset. + /// This may return null if the requested statistics has not been fetched before yet. + /// + /// The ruleset to return the corresponding for. + public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName); + + protected override void LoadComplete() + { + base.LoadComplete(); + + api.LocalUser.BindValueChanged(_ => + { + // queuing up requests directly on user change is unsafe, as the API status may have not been updated yet. + // schedule a frame to allow the API to be in its correct state sending requests. + Schedule(initialiseStatistics); + }, true); + } + + private void initialiseStatistics() + { + statisticsCache.Clear(); + + if (api.LocalUser.Value == null || api.LocalUser.Value.Id <= 1) + return; + + foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) + RefetchStatistics(ruleset); + } + + public void RefetchStatistics(RulesetInfo ruleset, Action? callback = null) + { + if (!ruleset.IsLegacyRuleset()) + throw new InvalidOperationException($@"Retrieving statistics is not supported for ruleset {ruleset.ShortName}"); + + var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset); + request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback); + api.Queue(request); + } + + protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action? callback = null) + { + var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName); + statisticsCache[ruleset.ShortName] = newStatistics; + + var update = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics); + callback?.Invoke(update); + StatisticsUpdated?.Invoke(update); + } + } + + public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics); +} diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 2ceb37fc01..dfc7a53fb2 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -7,7 +7,7 @@ using osu.Game.Online.API; namespace osu.Game.Online.Rooms { - public class JoinRoomRequest : APIRequest + public class JoinRoomRequest : APIRequest { public readonly Room Room; public readonly string? Password; diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 094fe4ce56..5a008bac13 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -366,12 +366,8 @@ namespace osu.Game.Online.Rooms { RoomID = other.RoomID; Name = other.Name; - Category = other.Category; - - if (other.Host != null && Host?.Id != other.Host.Id) - Host = other.Host; - + Host = other.Host; ChannelId = other.ChannelId; Status = other.Status; Availability = other.Availability; @@ -388,22 +384,10 @@ namespace osu.Game.Online.Rooms PlaylistItemStats = other.PlaylistItemStats; CurrentPlaylistItem = other.CurrentPlaylistItem; AutoSkip = other.AutoSkip; - - other.RemoveExpiredPlaylistItems(); - Playlist = other.Playlist; RecentParticipants = other.RecentParticipants; } - public void RemoveExpiredPlaylistItems() - { - // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, - // and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room. - // More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room. - if (Status is not RoomStatusEnded) - Playlist = Playlist.Where(i => !i.Expired).ToArray(); - } - [JsonObject(MemberSerialization.OptIn)] public class RoomPlaylistItemStats { diff --git a/osu.Game/Online/UserStatisticsUpdate.cs b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs similarity index 84% rename from osu.Game/Online/UserStatisticsUpdate.cs rename to osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs index f85b219ef0..dc55c57c68 100644 --- a/osu.Game/Online/UserStatisticsUpdate.cs +++ b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online /// /// Contains data about the change in a user's profile statistics after completing a score. /// - public class UserStatisticsUpdate + public class ScoreBasedUserStatisticsUpdate { /// /// The score set by the user that triggered the update. @@ -27,12 +27,12 @@ namespace osu.Game.Online public UserStatistics After { get; } /// - /// Creates a new . + /// Creates a new . /// /// The score set by the user that triggered the update. /// The user's profile statistics prior to the score being set. /// The user's profile statistics after the score was set. - public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) + public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) { Score = score; Before = before; diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index af32e86ae4..73ca3c9f53 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -2,18 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Scoring; -using osu.Game.Users; namespace osu.Game.Online { @@ -22,8 +18,10 @@ namespace osu.Game.Online /// public partial class UserStatisticsWatcher : Component { - public IBindable LatestUpdate => latestUpdate; - private readonly Bindable latestUpdate = new Bindable(); + private readonly LocalUserStatisticsProvider statisticsProvider; + + public IBindable LatestUpdate => latestUpdate; + private readonly Bindable latestUpdate = new Bindable(); [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -33,13 +31,15 @@ namespace osu.Game.Online private readonly Dictionary watchedScores = new Dictionary(); - private Dictionary? latestStatistics; + public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider) + { + this.statisticsProvider = statisticsProvider; + } protected override void LoadComplete() { base.LoadComplete(); - api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true); spectatorClient.OnUserScoreProcessed += userScoreProcessed; } @@ -61,35 +61,6 @@ namespace osu.Game.Online }); } - private void onUserChanged(APIUser? localUser) => Schedule(() => - { - latestStatistics = null; - - if (localUser == null || localUser.OnlineID <= 1) - return; - - var userRequest = new GetUsersRequest(new[] { localUser.OnlineID }); - userRequest.Success += initialiseUserStatistics; - api.Queue(userRequest); - }); - - private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() => - { - var user = response.Users.SingleOrDefault(); - - // possible if the user is restricted or similar. - if (user == null) - return; - - latestStatistics = new Dictionary(); - - if (user.RulesetsStatistics != null) - { - foreach (var rulesetStats in user.RulesetsStatistics) - latestStatistics.Add(rulesetStats.Key, rulesetStats.Value); - } - }); - private void userScoreProcessed(int userId, long scoreId) { if (userId != api.LocalUser.Value?.OnlineID) @@ -98,30 +69,11 @@ namespace osu.Game.Online if (!watchedScores.Remove(scoreId, out var scoreInfo)) return; - requestStatisticsUpdate(userId, scoreInfo); - } - - private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) - { - var request = new GetUserRequest(userId, scoreInfo.Ruleset); - request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics)); - api.Queue(request); - } - - private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics) - { - string rulesetName = scoreInfo.Ruleset.ShortName; - - api.UpdateStatistics(updatedStatistics); - - if (latestStatistics == null) - return; - - latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics); - latestRulesetStatistics ??= new UserStatistics(); - - latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); - latestStatistics[rulesetName] = updatedStatistics; + statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() => + { + if (u.OldStatistics != null) + latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics); + })); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dce24c6ee7..a92b1f4d36 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -148,8 +148,7 @@ namespace osu.Game [Resolved] private FrameworkConfigManager frameworkConfig { get; set; } - [Cached] - private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); + private DifficultyRecommender difficultyRecommender; [Cached] private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); @@ -1069,7 +1068,11 @@ namespace osu.Game ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); }); - loadComponentSingleFile(new UserStatisticsWatcher(), Add, true); + LocalUserStatisticsProvider statisticsProvider; + + loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true); + loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true); + loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true); loadComponentSingleFile(Toolbar = new Toolbar { OnHome = delegate @@ -1139,7 +1142,6 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); - Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index b47e2b82c0..f83368fa41 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -198,7 +198,6 @@ namespace osu.Game.Overlays { c.Anchor = Anchor.TopCentre; c.Origin = Anchor.TopCentre; - c.Scale = new Vector2(0.8f); })).ToArray(); private static ReverseChildIDFillFlowContainer createCardContainerFor(IEnumerable newCards) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index fc0060d86a..f027888962 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList private readonly Dictionary channelMap = new Dictionary(); + public ChannelGroup AnnounceChannelGroup { get; private set; } = null!; + public ChannelGroup PublicChannelGroup { get; private set; } = null!; + public ChannelGroup PrivateChannelGroup { get; private set; } = null!; + private OsuScrollContainer scroll = null!; private SearchContainer groupFlow = null!; - private ChannelGroup announceChannelGroup = null!; - private ChannelGroup publicChannelGroup = null!; - private ChannelGroup privateChannelGroup = null!; + private ChannelListItem selector = null!; private TextBox searchTextBox = null!; @@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), - publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), + AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), + PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), - privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()), + PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, }, @@ -111,69 +113,70 @@ namespace osu.Game.Overlays.Chat.ChannelList item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); - FillFlowContainer flow = getFlowForChannel(channel); + ChannelGroup group = getGroupFromChannel(channel); channelMap.Add(channel, item); - flow.Add(item); + group.AddChannel(item); updateVisibility(); } public void RemoveChannel(Channel channel) { - if (!channelMap.ContainsKey(channel)) + if (!channelMap.TryGetValue(channel, out var item)) return; - ChannelListItem item = channelMap[channel]; - FillFlowContainer flow = getFlowForChannel(channel); + ChannelGroup group = getGroupFromChannel(channel); channelMap.Remove(channel); - flow.Remove(item, true); + group.RemoveChannel(item); updateVisibility(); } public ChannelListItem GetItem(Channel channel) { - if (!channelMap.ContainsKey(channel)) + if (!channelMap.TryGetValue(channel, out var item)) throw new ArgumentOutOfRangeException(); - return channelMap[channel]; + return item; } public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); - private FillFlowContainer getFlowForChannel(Channel channel) + private ChannelGroup getGroupFromChannel(Channel channel) { switch (channel.Type) { case ChannelType.Public: - return publicChannelGroup.ItemFlow; + return PublicChannelGroup; case ChannelType.PM: - return privateChannelGroup.ItemFlow; + return PrivateChannelGroup; case ChannelType.Announce: - return announceChannelGroup.ItemFlow; + return AnnounceChannelGroup; default: - return publicChannelGroup.ItemFlow; + return PublicChannelGroup; } } private void updateVisibility() { - if (announceChannelGroup.ItemFlow.Children.Count == 0) - announceChannelGroup.Hide(); + if (AnnounceChannelGroup.ItemFlow.Children.Count == 0) + AnnounceChannelGroup.Hide(); else - announceChannelGroup.Show(); + AnnounceChannelGroup.Show(); } - private partial class ChannelGroup : FillFlowContainer + public partial class ChannelGroup : FillFlowContainer { - public readonly FillFlowContainer ItemFlow; + private readonly bool sortByRecent; + public readonly ChannelListItemFlow ItemFlow; - public ChannelGroup(LocalisableString label) + public ChannelGroup(LocalisableString label, bool sortByRecent) { + this.sortByRecent = sortByRecent; Direction = FillDirection.Vertical; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -187,7 +190,7 @@ namespace osu.Game.Overlays.Chat.ChannelList Margin = new MarginPadding { Left = 18, Bottom = 5 }, Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), }, - ItemFlow = new FillFlowContainer + ItemFlow = new ChannelListItemFlow(sortByRecent) { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, @@ -195,6 +198,60 @@ namespace osu.Game.Overlays.Chat.ChannelList }, }; } + + public partial class ChannelListItemFlow : FillFlowContainer + { + private readonly bool sortByRecent; + + public ChannelListItemFlow(bool sortByRecent) + { + this.sortByRecent = sortByRecent; + } + + public void Reflow() => InvalidateLayout(); + + public override IEnumerable FlowingChildren => sortByRecent + ? base.FlowingChildren.OfType().OrderByDescending(i => i.Channel.LastMessageId ?? long.MinValue) + : base.FlowingChildren.OfType().OrderBy(i => i.Channel.Name); + } + + public void AddChannel(ChannelListItem item) + { + ItemFlow.Add(item); + + if (sortByRecent) + { + item.Channel.NewMessagesArrived += newMessagesArrived; + item.Channel.PendingMessageResolved += pendingMessageResolved; + } + + ItemFlow.Reflow(); + } + + public void RemoveChannel(ChannelListItem item) + { + if (sortByRecent) + { + item.Channel.NewMessagesArrived -= newMessagesArrived; + item.Channel.PendingMessageResolved -= pendingMessageResolved; + } + + ItemFlow.Remove(item, true); + } + + private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow(); + private void newMessagesArrived(IEnumerable _) => ItemFlow.Reflow(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + foreach (var item in ItemFlow) + { + item.Channel.NewMessagesArrived -= newMessagesArrived; + item.Channel.PendingMessageResolved -= pendingMessageResolved; + } + } } private partial class ChannelSearchTextBox : BasicSearchTextBox diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index b237a0ee05..df657aa55b 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps ? new BeatmapCardNormal(model) { Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Origin = Anchor.TopCentre } : null; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 83e9140b33..779d5cdf00 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GraphicsSettingsStrings.HitLighting, Current = config.GetBindable(OsuSetting.HitLighting) }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.StarFountains, + Current = config.GetBindable(OsuSetting.StarFountains) + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index f4dd319152..b4caaf7983 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysRequireHoldForMenu, + Current = config.GetBindable(OsuSetting.AlwaysRequireHoldingForPause), + }, + new SettingsCheckbox { LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton, Current = config.GetBindable(OsuSetting.AlwaysShowHoldForMenuButton), diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index ce087f1807..f40a4c941f 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -268,7 +268,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private void updateScreenModeWarning() { - if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS) + // Can be removed once we stop supporting SDL2. + if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && !FrameworkEnvironment.UseSDL3) { if (windowModeDropdown.Current.Value == WindowMode.Fullscreen) windowModeDropdown.SetNoticeText(LayoutSettingsStrings.FullscreenMacOSNote, true); diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index 07c2e72774..d5891da936 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable { - public Bindable LatestUpdate { get; } = new Bindable(); + public Bindable LatestUpdate { get; } = new Bindable(); private Statistic globalRank = null!; private Statistic pp = null!; @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar }; if (userStatisticsWatcher != null) - ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); + ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 19554b6504..4ca937bf86 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mods { foreach (var hitObject in hitObjects) { - if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) + if (hitObject.HitWindows != HitWindows.Empty) yield return hitObject; foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects)) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index fc4eef13ba..269342460f 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { @@ -20,32 +21,36 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateUnstableRate(this IEnumerable hitEvents) + public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList hitEvents, UnstableRateCalculationResult? result = null) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); - int count = 0; - double mean = 0; - double sumOfSquares = 0; + result ??= new UnstableRateCalculationResult(); - foreach (var e in hitEvents) + // Handle rewinding in the simplest way possible. + if (hitEvents.Count < result.EventCount + 1) + result = new UnstableRateCalculationResult(); + + for (int i = result.EventCount; i < hitEvents.Count; i++) { + HitEvent e = hitEvents[i]; + if (!AffectsUnstableRate(e)) continue; - count++; + result.EventCount++; // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double currentValue = e.TimeOffset / e.GameplayRate!.Value; - double nextMean = mean + (currentValue - mean) / count; - sumOfSquares += (currentValue - mean) * (currentValue - nextMean); - mean = nextMean; + double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount; + result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean); + result.Mean = nextMean; } - if (count == 0) + if (result.EventCount == 0) return null; - return 10.0 * Math.Sqrt(sumOfSquares / count); + return result; } /// @@ -65,6 +70,39 @@ namespace osu.Game.Rulesets.Scoring return timeOffsets.Average(); } - public static bool AffectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); + public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result); + public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit(); + + /// + /// Data type returned by which allows efficient incremental processing. + /// + /// + /// This should be passed back into future calls as a parameter. + /// + /// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session. + /// When a new gameplay session is started, any existing results should be disposed. + /// + public class UnstableRateCalculationResult + { + /// + /// Total events processed. For internal incremental calculation use. + /// + public int EventCount; + + /// + /// Last sum-of-squares value. For internal incremental calculation use. + /// + public double SumOfSquares; + + /// + /// Last mean value. For internal incremental calculation use. + /// + public double Mean; + + /// + /// The unstable rate. + /// + public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount); + } } } diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 2d008b58ba..a6a268fc78 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Scoring /// An empty with only and . /// No time values are provided (meaning instantaneous hit or miss). /// - public static HitWindows Empty => new EmptyHitWindows(); + public static HitWindows Empty { get; } = new EmptyHitWindows(); public HitWindows() { @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual DifficultyRange[] GetRanges() => base_ranges; - public class EmptyHitWindows : HitWindows + private class EmptyHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index e3601fe91e..3177873182 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Scoring /// Perform a lookup query on available s. /// /// The query. - /// The first result for the provided query, or null if no results were found. + /// The first result for the provided query in its detached form, or null if no results were found. public ScoreInfo? Query(Expression> query) { return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); @@ -88,8 +88,14 @@ namespace osu.Game.Scoring { ScoreInfo? databasedScoreInfo = null; - if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash)) - databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); + if (originalScoreInfo is ScoreInfo scoreInfo) + { + if (scoreInfo.IsManaged) + return scoreInfo.Detach(); + + if (!string.IsNullOrEmpty(scoreInfo.Hash)) + databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); + } if (originalScoreInfo.OnlineID > 0) databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 6724a1dc4d..78cee2c1cf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -258,6 +258,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void resetTernaryStates() { + if (SelectedItems.Count > 0) + return; + SelectionNewComboState.Value = TernaryState.False; AutoSelectionBankEnabled.Value = true; SelectionAdditionBanksEnabled.Value = true; diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index da349373c3..3fc5fe57fb 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -7,12 +7,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.Menu { @@ -78,50 +80,49 @@ namespace osu.Game.Screens.Menu static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - string tip = getRandomTip(); + var tip = getRandomTip(); textFlow.Clear(); - textFlow.AddParagraph("a tip for you:", formatSemiBold); + textFlow.AddParagraph(MenuTipStrings.MenuTipTitle, formatSemiBold); textFlow.AddParagraph(tip, formatRegular); this.FadeInFromZero(200, Easing.OutQuint) - .Delay(1000 + 80 * tip.Length) + .Delay(1000 + 80 * tip.ToString().Length) .Then() .FadeOutFromOne(2000, Easing.OutQuint); } - private string getRandomTip() + private LocalisableString getRandomTip() { - string[] tips = + LocalisableString[] tips = { - "Press Ctrl-T anywhere in the game to toggle the toolbar!", - "Press Ctrl-O anywhere in the game to access options!", - "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", - "New features are coming online every update. Make sure to stay up-to-date!", - "If you find the UI too large or small, try adjusting UI scale in settings!", - "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", - "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!", - "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", - "Try scrolling right in mod select to find a bunch of new fun mods!", - "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", - "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", - "Toggle advanced frame / thread statistics with Ctrl-F11!", - "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", - "You can pause during a replay by pressing Space!", - "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", - "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", - "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!", - "Drag and drop any image into the skin editor to load it in quickly!", - "You can create mod presets to make toggling your favorite mod combinations easier!", - "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", - "Press Ctrl-Shift-R to switch to a random skin!", - "Press Ctrl-Shift-F to toggle the FPS Counter. But make sure not to pay too much attention to it!", - "While watching a replay, press Ctrl-H to toggle replay settings!", - "You can easily copy the mods from scores on a leaderboard by right-clicking on them!", - "Ctrl-Enter at song select will start a beatmap in autoplay mode!" + MenuTipStrings.ToggleToolbarShortcut, + MenuTipStrings.GameSettingsShortcut, + MenuTipStrings.DynamicSettings, + MenuTipStrings.NewFeaturesAreComingOnline, + MenuTipStrings.UIScalingSettings, + MenuTipStrings.ScreenScalingSettings, + MenuTipStrings.FreeOsuDirect, + MenuTipStrings.ReplaySeeking, + MenuTipStrings.MultithreadingSupport, + MenuTipStrings.TryNewMods, + MenuTipStrings.EmbeddedWebContent, + MenuTipStrings.BeatmapRightClick, + MenuTipStrings.TemporaryDeleteOperations, + MenuTipStrings.DiscoverPlaylists, + MenuTipStrings.ToggleAdvancedFPSCounter, + MenuTipStrings.GlobalStatisticsShortcut, + MenuTipStrings.ReplayPausing, + MenuTipStrings.ConfigurableHotkeys, + MenuTipStrings.PeekHUDWhenHidden, + MenuTipStrings.SkinEditor, + MenuTipStrings.DragAndDropImageInSkinEditor, + MenuTipStrings.ModPresets, + MenuTipStrings.ModCustomisationSettings, + MenuTipStrings.RandomSkinShortcut, + MenuTipStrings.ToggleReplaySettingsShortcut, + MenuTipStrings.CopyModsFromScore, + MenuTipStrings.AutoplayBeatmapShortcut }; return tips[RNG.Next(0, tips.Length)]; diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index 3bdc0efe19..3aac365eee 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -8,6 +8,9 @@ using osu.Game.Graphics.Sprites; using osuTK; using osu.Game.Graphics; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -27,26 +30,60 @@ namespace osu.Game.Screens.Menu public SongTicker() { AutoSizeAxes = Axes.Both; - Child = new FillFlowContainer + InternalChildren = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), - Children = new Drawable[] + new Container { - title = new OsuSpriteText + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Position = new Vector2(5, -5), + Padding = new MarginPadding(-5), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true) - }, - artist = new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Font = OsuFont.GetFont(size: 16) + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Radius = 75, + Type = EdgeEffectType.Shadow, + Colour = OsuColour.Gray(0.04f).Opacity(0.3f), + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + }, + } + }, } - } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + Children = new Drawable[] + { + title = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true) + }, + artist = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 16) + } + } + }, }; } diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index b10ce8ed1b..b213d424df 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -60,10 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } foreach (var incoming in result) - { - incoming.RemoveExpiredPlaylistItems(); RoomManager.AddOrUpdateRoom(incoming); - } initialRoomsReceived.Value = true; tcs.SetResult(true); diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index ca42e98e3c..73f980f0a3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -72,9 +72,13 @@ namespace osu.Game.Screens.OnlinePlay.Components currentJoinRoomRequest?.Cancel(); currentJoinRoomRequest = new JoinRoomRequest(room, password); - currentJoinRoomRequest.Success += () => + currentJoinRoomRequest.Success += result => { joinedRoom.Value = room; + + AddOrUpdateRoom(result); + room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. + onSuccess?.Invoke(room); }; diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 8b80228ae1..7cee8b3546 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Success += result => { - result.RemoveExpiredPlaylistItems(); RoomManager.AddOrUpdateRoom(result); tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 0dc7e7930a..13a282dd52 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void presentScore(long id) { if (this.IsCurrentScreen()) - this.Push(new PlaylistItemScoreResultsScreen(room.RoomID!.Value, playlistItem, id)); + this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem)); } private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 1dbef079d4..79617f172c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -438,7 +438,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match => MaxParticipantsField.Text = room.MaxParticipants?.ToString(); private void updateRoomAutoStartDuration() - => typeLabel.Text = room.AutoStartDuration.GetLocalisableDescription(); + => startModeDropdown.Current.Value = (StartMode)room.AutoStartDuration.TotalSeconds; private void updateRoomPlaylist() => drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index c439df82a6..6b3e8fea46 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen + public partial class MultiplayerResultsScreen : PlaylistItemScoreResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index dc06b88823..81ae51bd1b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -191,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - // Invoke callback to add the scores. - callback.Invoke(scoreInfos); + // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 32be7f21b0..05c03a4b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -11,13 +11,19 @@ using osu.Game.Scoring; namespace osu.Game.Screens.OnlinePlay.Playlists { /// - /// Shows a selected arbitrary score for a playlist item, with scores around included. + /// Shows a given score in a playlist item, with scores around included. /// public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen { private readonly long scoreId; - public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId) + public PlaylistItemScoreResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem) + { + scoreId = score.OnlineID; + } + + public PlaylistItemScoreResultsScreen(long scoreId, long roomId, PlaylistItem playlistItem) : base(null, roomId, playlistItem) { this.scoreId = scoreId; @@ -28,9 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); - - Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); - + Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs new file mode 100644 index 0000000000..5b20496dba --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -0,0 +1,41 @@ +// 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.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + /// + /// Shows a user's best score in a playlist item, with scores around included. + /// + public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen + { + private readonly int userId; + + public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId) + : base(null, roomId, playlistItem) + { + this.userId = userId; + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + { + var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + + Schedule(() => + { + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault(); + }); + + return scoreInfos; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs deleted file mode 100644 index e038cf3288..0000000000 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.Game.Online.API; -using osu.Game.Online.Rooms; -using osu.Game.Scoring; - -namespace osu.Game.Screens.OnlinePlay.Playlists -{ - /// - /// Shows the user's best score for a given playlist item, with scores around included. - /// - public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen - { - public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) - : base(score, roomId, playlistItem) - { - } - - protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); - - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) - { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) - { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); - } - - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - return scoreInfos; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 7ca09b5563..b82c2404ab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID != null); - return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) + return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem) { AllowRetry = true, ShowUserStatistics = true, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index bac99123d5..61df331c48 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.Cursor; using osu.Game.Input; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; @@ -34,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private readonly IBindable isIdle = new BindableBool(); + [Resolved] + private IAPIProvider api { get; set; } = null!; + [Resolved(CanBeNull = true)] private IdleTracker? idleTracker { get; set; } @@ -145,7 +149,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestResults = item => { Debug.Assert(Room.RoomID != null); - ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item)); + ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id)); } } }, diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 806985e19d..96e937fda7 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -162,14 +162,18 @@ namespace osu.Game.Screens.Play.HUD private bool pendingAnimation; private ScheduledDelegate shakeOperation; + private Bindable alwaysRequireHold; + public HoldButton(bool isDangerousAction) : base(isDangerousAction) { } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuConfigManager config) { + alwaysRequireHold = config.GetBindable(OsuSetting.AlwaysRequireHoldingForPause); + Size = new Vector2(60); Child = new CircularContainer @@ -299,7 +303,13 @@ namespace osu.Game.Screens.Play.HUD { case GlobalAction.Back: if (!pendingAnimation) - Confirm(); + { + if (IsDangerousAction || alwaysRequireHold.Value) + BeginConfirm(); + else + Confirm(); + } + return true; case GlobalAction.PauseGameplay: @@ -307,7 +317,13 @@ namespace osu.Game.Screens.Play.HUD if (ReplayLoaded.Value) return false; if (!pendingAnimation) - Confirm(); + { + if (IsDangerousAction || alwaysRequireHold.Value) + BeginConfirm(); + else + Confirm(); + } + return true; } diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index ab7ab6b3a0..a856a09388 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); + private HitEventExtensions.UnstableRateCalculationResult? unstableRateResult; + [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; @@ -44,9 +46,6 @@ namespace osu.Game.Screens.Play.HUD DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); } - private bool changesUnstableRate(JudgementResult judgement) - => !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit; - protected override void LoadComplete() { base.LoadComplete(); @@ -56,13 +55,20 @@ namespace osu.Game.Screens.Play.HUD updateDisplay(); } - private void updateDisplay(JudgementResult _) => Scheduler.AddOnce(updateDisplay); + private void updateDisplay(JudgementResult result) + { + if (HitEventExtensions.AffectsUnstableRate(result.HitObject, result.Type)) + Scheduler.AddOnce(updateDisplay); + } private void updateDisplay() { - double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate(); + unstableRateResult = scoreProcessor.HitEvents.CalculateUnstableRate(unstableRateResult); + + double? unstableRate = unstableRateResult?.Result; valid.Value = unstableRate != null; + if (unstableRate != null) Current.Value = (int)Math.Round(unstableRate.Value); } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 7659c61123..fd9596c838 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -5,8 +5,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; @@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + private Bindable kiaiStarFountains = null!; + [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { + kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); + RelativeSizeAxes = Axes.Both; Children = new[] @@ -48,6 +54,9 @@ namespace osu.Game.Screens.Play { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + if (!kiaiStarFountains.Value) + return; + if (effectPoint.KiaiMode && !isTriggered) { bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e9722350bd..2d1f602832 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -976,7 +976,9 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - failAnimationContainer.Start(); + bool restartOnFail = GameplayState.Mods.OfType().Any(m => m.RestartOnFail); + if (!restartOnFail) + failAnimationContainer.Start(); // Failures can be triggered either by a judgement, or by a mod. // @@ -990,7 +992,7 @@ namespace osu.Game.Screens.Play ScoreProcessor.FailScore(Score.ScoreInfo); OnFail(); - if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) + if (restartOnFail) Restart(true); }); } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 3e36c630db..0db96b71ad 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -28,6 +28,7 @@ using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Performance; +using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Skinning; @@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play private FillFlowContainer disclaimers = null!; private OsuScrollContainer settingsScroll = null!; + private Bindable lastScore = null!; + private Bindable showStoryboards = null!; private bool backgroundBrightnessReduction; @@ -179,6 +182,8 @@ namespace osu.Game.Screens.Play { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); + lastScore = sessionStatics.GetBindable(Static.LastLocalUserScore); + showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard); const float padding = 25; @@ -347,6 +352,8 @@ namespace osu.Game.Screens.Play highPerformanceSession?.Dispose(); highPerformanceSession = null; + lastScore.Value = null; + return base.OnExiting(e); } diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index 4f665b87e8..e5c9e115d1 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play private readonly Func>? importFailedScore; - private ScoreInfo? importedScore; + private Live? importedScore; private DownloadButton button = null!; @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play switch (state.Value) { case DownloadState.LocallyAvailable: - game?.PresentScore(importedScore, ScorePresentType.Gameplay); + game?.PresentScore(importedScore?.Value, ScorePresentType.Gameplay); break; case DownloadState.NotDownloaded: @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play { Task.Run(importFailedScore).ContinueWith(t => { - importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); + importedScore = realm.Run?>(r => r.Find(t.GetResultSafely().ID)?.ToLive(realm)); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); }).FireAndForget(); } @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Play if (player != null) { - importedScore = realm.Run(r => r.Find(player.Score.ScoreInfo.ID)?.Detach()); + importedScore = realm.Run(r => r.Find(player.Score.ScoreInfo.ID)?.ToLive(realm)); state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded; } @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play { if (state.NewValue != DownloadState.LocallyAvailable) return; - if (importedScore != null) scoreManager.Export(importedScore); + if (importedScore != null) scoreManager.Export(importedScore.Value); this.state.ValueChanged -= exportWhenReady; } diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index a9b93e0ffc..a80aeaa5dd 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsBasic() && e.Result.IsHit()).ToList(); + this.hitEvents = hitEvents.Where(e => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsBasic() && e.Result.IsHit()).ToList(); bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary()).ToArray>(); } diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index cc3535a426..d114bed156 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -15,10 +15,10 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates and computes an statistic. /// /// Sequence of s to calculate the unstable rate based on. - public UnstableRate(IEnumerable hitEvents) + public UnstableRate(IReadOnlyList hitEvents) : base("Unstable Rate") { - Value = hitEvents.CalculateUnstableRate(); + Value = hitEvents.CalculateUnstableRate()?.Result; } protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 1e60e09486..171a3f0f65 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { private const float transition_duration = 300; - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; private GridContainer content = null!; @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User FinishTransforms(true); } - private void onUpdateReceived(ValueChangedEvent update) + private void onUpdateReceived(ValueChangedEvent update) { if (update.NewValue == null) { diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index e5f07d9891..e6a6530345 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { public abstract partial class RankingChangeRow : CompositeDrawable { - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private readonly Func accessor; @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true); } - private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) + private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) { var update = statisticsUpdate.NewValue; diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index 4e9c07ab7b..86fed4a9bb 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics { private readonly ScoreInfo achievedScore; - internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); + internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); - private IBindable latestGlobalStatisticsUpdate = null!; + private IBindable latestGlobalStatisticsUpdate = null!; public UserStatisticsPanel(ScoreInfo achievedScore) { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5e1e0ce615..fc7c7989e2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -29,7 +29,6 @@ using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; using osuTK; using osuTK.Input; -using Realms; namespace osu.Game.Screens.Select { @@ -207,8 +206,6 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - private IDisposable? subscriptionBeatmaps; - private readonly DrawablePool setPool = new DrawablePool(100); private Sample? spinSample; @@ -258,13 +255,6 @@ namespace osu.Game.Screens.Select } } - protected override void LoadComplete() - { - base.LoadComplete(); - - subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); - } - private readonly HashSet setsRequiringUpdate = new HashSet(); private readonly HashSet setsRequiringRemoval = new HashSet(); @@ -366,35 +356,6 @@ namespace osu.Game.Screens.Select BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find(id); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) - { - // we only care about actual changes in hidden status. - if (changes == null) - return; - - bool changed = false; - - foreach (int i in changes.InsertedIndices) - { - var beatmapInfo = sender[i]; - var beatmapSet = beatmapInfo.BeatmapSet; - - Debug.Assert(beatmapSet != null); - - // Only require to action here if the beatmap is missing. - // This avoids processing these events unnecessarily when new beatmaps are imported, for example. - if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) - && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) - { - updateBeatmapSet(beatmapSet.Detach()); - changed = true; - } - } - - if (changed) - invalidateAfterChange(); - } - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { removeBeatmapSet(beatmapSet.ID); @@ -1292,12 +1253,5 @@ namespace osu.Game.Screens.Select return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - subscriptionBeatmaps?.Dispose(); - } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 224bb90c8c..c9149bda22 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay return true; } - joinRoomRequest.TriggerSuccess(); + joinRoomRequest.TriggerSuccess(createResponseRoom(room, true)); return true; } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index c27e7f15ca..a311531088 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -78,12 +78,12 @@ namespace osu.Game.Tests.Visual.Spectator /// The spectator state to end play with. public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { - if (!userBeatmapDictionary.ContainsKey(userId)) + if (!userBeatmapDictionary.TryGetValue(userId, out int value)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { - BeatmapID = userBeatmapDictionary[userId], + BeatmapID = value, RulesetID = 0, Mods = userModsDictionary[userId], State = state diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 93812e3f6b..a8e0fc9030 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -41,6 +41,12 @@ namespace osu.Game.Users public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; + /// + /// Returns the ID of the beatmap involved in this activity, if applicable and/or available. + /// + /// + public virtual int? GetBeatmapID(bool hideIdentifiableInformation = false) => null; + [MessagePackObject] public class ChoosingBeatmap : UserActivity { @@ -76,6 +82,7 @@ namespace osu.Game.Users public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => BeatmapID; } [MessagePackObject] @@ -156,6 +163,11 @@ namespace osu.Game.Users // For now let's assume that showing the beatmap a user is editing could reveal unwanted information. ? string.Empty : BeatmapDisplayTitle; + + public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => hideIdentifiableInformation + // For now let's assume that showing the beatmap a user is editing could reveal unwanted information. + ? null + : BeatmapID; } [MessagePackObject] diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 6d8d7dd143..5e3ae172be 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -4,13 +4,16 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Game.Online.API; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Users @@ -24,13 +27,9 @@ namespace osu.Game.Users private const int padding = 10; private const int main_content_height = 80; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private ProfileValueDisplay globalRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!; - - private readonly IBindable statistics = new Bindable(); + private LoadingLayer loadingLayer = null!; public UserRankPanel(APIUser user) : base(user) @@ -43,13 +42,37 @@ namespace osu.Game.Users private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + } - statistics.BindTo(api.Statistics); - statistics.BindValueChanged(stats => - { - globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; - countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; - }, true); + [Resolved] + private LocalUserStatisticsProvider? statisticsProvider { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (statisticsProvider != null) + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; + + ruleset.BindValueChanged(_ => updateDisplay(), true); + } + + private void onStatisticsUpdated(UserStatisticsUpdate update) + { + if (update.Ruleset.Equals(ruleset.Value)) + updateDisplay(); + } + + private void updateDisplay() + { + var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value); + + loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden; + globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; } protected override Drawable CreateLayout() @@ -176,7 +199,8 @@ namespace osu.Game.Users } } } - } + }, + loadingLayer = new LoadingLayer(true), } }; @@ -205,5 +229,13 @@ namespace osu.Game.Users } protected override Drawable? CreateBackground() => null; + + protected override void Dispose(bool isDisposing) + { + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + + base.Dispose(isDisposing); + } } }