From e3137d575bbea46423c8124a9ade25fab18d03f2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Nov 2023 01:11:17 +0900 Subject: [PATCH 01/51] Fix osu! and base HP processor break time implementation --- .../TestSceneOsuHealthProcessor.cs | 94 +++++++++++++++++++ .../Scoring/OsuHealthProcessor.cs | 25 ++--- .../TestSceneDrainingHealthProcessor.cs | 81 +++++++++++++++- .../Scoring/DrainingHealthProcessor.cs | 44 ++++----- 4 files changed, 203 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs new file mode 100644 index 0000000000..f810bbf155 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs @@ -0,0 +1,94 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneOsuHealthProcessor + { + [Test] + public void TestNoBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 5802f8fc0d..3207c7fb51 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -3,9 +3,7 @@ using System; using System.Linq; -using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -64,25 +62,16 @@ namespace osu.Game.Rulesets.Osu.Scoring { HitObject h = Beatmap.HitObjects[i]; - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614). - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; } - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + reduceHp(testDrop * (h.StartTime - lastTime)); lastTime = h.GetEndTime(); diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index fd0bff101f..584a9e09c0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddAssert("not failed", () => !processor.HasFailed); - AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); + AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", + () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddAssert("failed", () => processor.HasFailed); } @@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay assertHealthEqualTo(1); } + [Test] + public void TestNoBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index a8808d08e5..3d4fb862fb 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -103,18 +103,20 @@ namespace osu.Game.Rulesets.Scoring if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( - beatmap.HitObjects - .Select(hitObject => hitObject.GetEndTime()) - .Where(endTime => endTime <= breakPeriod.StartTime) - .DefaultIfEmpty(double.MinValue) - .Last(), - beatmap.HitObjects - .Select(hitObject => hitObject.StartTime) - .Where(startTime => startTime >= breakPeriod.EndTime) - .DefaultIfEmpty(double.MaxValue) - .First() - ))); + noDrainPeriodTracker = new PeriodTracker( + beatmap.Breaks.Select(breakPeriod => + new Period( + beatmap.HitObjects + .Select(hitObject => hitObject.GetEndTime()) + .Where(endTime => endTime <= breakPeriod.StartTime) + .DefaultIfEmpty(double.MinValue) + .Last(), + beatmap.HitObjects + .Select(hitObject => hitObject.StartTime) + .Where(startTime => startTime >= breakPeriod.EndTime) + .DefaultIfEmpty(double.MaxValue) + .First() + ))); targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); @@ -161,26 +163,24 @@ namespace osu.Game.Rulesets.Scoring { double currentHealth = 1; double lowestHealth = 1; - int currentBreak = -1; + int currentBreak = 0; for (int i = 0; i < healthIncreases.Count; i++) { double currentTime = healthIncreases[i].time; double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime; - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime) { - // Advance the last break occuring before the current time - while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime) - currentBreak++; - - if (currentBreak >= 0) - lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime); + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = currentTime; + currentBreak++; } // Apply health adjustments - currentHealth -= (healthIncreases[i].time - lastTime) * result; + currentHealth -= (currentTime - lastTime) * result; lowestHealth = Math.Min(lowestHealth, currentHealth); currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); From f7fce1d714dc4d849aef066abe62d1feb4a3145f Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 22 Nov 2023 09:55:32 +0100 Subject: [PATCH 02/51] Fix freehand-drawn sliders with distance snap --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 63370350ed..c35c6fdf95 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) return base.OnDragStart(e); + HitObject.Position = ToLocalSpace(e.ScreenSpaceMouseDownPosition); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); state = SliderPlacementState.Drawing; return true; From 95b12082aed0bb40e998f8b0402be77cfb4639e1 Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 22 Nov 2023 10:14:44 +0100 Subject: [PATCH 03/51] Update to respect distance snap This will cause freehand drawing to respect distance snap, instead changing the drawn path to start from the sliders start position and "snap" in a linear fashion to the cursor position from the position indicated by distance snap --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index c35c6fdf95..ac7b5e63e1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) return base.OnDragStart(e); - HitObject.Position = ToLocalSpace(e.ScreenSpaceMouseDownPosition); + bSplineBuilder.AddLinearPoint(Vector2.Zero); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); state = SliderPlacementState.Drawing; return true; From 74966cae6ad9a98454731f19123a7044ff2d7e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 10:31:58 +0900 Subject: [PATCH 04/51] Remove unused using directive --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs index f810bbf155..16f28c0212 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs @@ -1,7 +1,6 @@ // 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; From 61d5a890f724a9bca05fd8e0842e47eeae7626a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Nov 2023 12:37:02 +0900 Subject: [PATCH 05/51] Update links to figma library --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f7d88f5c7..4106641adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. -If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library). +If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library). Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes: @@ -85,4 +85,4 @@ If you're uncertain about some part of the codebase or some inner workings of th - [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on - [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game - [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game -- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu! +- [Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library): Contains finished and draft designs for osu! From 537b0ae870d3fb4e1023c8418ce7badb71cb8cb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 12:47:40 +0900 Subject: [PATCH 06/51] Add silly annotation for now (more new r# rules) --- osu.Game/Screens/Ranking/ScorePanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 1d332d6b27..1f7ba3692a 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -82,6 +83,7 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); + [CanBeNull] public event Action StateChanged; /// From 95c00f966627c00648ed4c72848962676aad8eff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 12:45:55 +0900 Subject: [PATCH 07/51] Add `HexaconIcons` lookup to allow usage with `SpriteIcon` --- osu.Game/Graphics/HexaconsIcons.cs | 112 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 + .../Play/HUD/ArgonCounterTextComponent.cs | 2 +- osu.Game/Skinning/LegacySpriteText.cs | 2 +- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Graphics/HexaconsIcons.cs diff --git a/osu.Game/Graphics/HexaconsIcons.cs b/osu.Game/Graphics/HexaconsIcons.cs new file mode 100644 index 0000000000..9b0fb30963 --- /dev/null +++ b/osu.Game/Graphics/HexaconsIcons.cs @@ -0,0 +1,112 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Text; + +namespace osu.Game.Graphics +{ + public static class HexaconsIcons + { + public const string FONT_NAME = "Icons/Hexacons"; + + public static IconUsage Editor => get(HexaconsMapping.editor); + + private static IconUsage get(HexaconsMapping icon) + { + return new IconUsage((char)icon, FONT_NAME); + } + + // Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility. + // Names should match filenames in resources. + private enum HexaconsMapping + { + beatmap_packs, + beatmap, + calendar, + chart, + community, + contests, + devtools, + download, + editor, + featured_artist, + home, + messaging, + music, + news, + notification, + profile, + rankings, + search, + settings, + social, + store, + tournament, + wiki, + } + + public class HexaconsStore : ITextureStore, ITexturedGlyphLookupStore + { + private readonly TextureStore textures; + + public HexaconsStore(TextureStore textures) + { + this.textures = textures; + } + + public void Dispose() + { + textures.Dispose(); + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + if (fontName == FONT_NAME) + return new Glyph(textures.Get($"{fontName}/{((HexaconsMapping)character).ToString().Replace("_", "-")}")); + + return null; + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + + public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public Texture Get(string name) => throw new NotImplementedException(); + + public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public Task GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public class Glyph : ITexturedCharacterGlyph + { + public float XOffset => default; + public float YOffset => default; + public float XAdvance => default; + public float Baseline => default; + public char Character => default; + + public float GetKerning(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException(); + + public Texture Texture { get; } + public float Width => Texture.Width; + public float Height => Texture.Height; + + public Glyph(Texture texture) + { + Texture = texture; + } + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 228edc8952..2d8024a45a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -477,6 +477,8 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Light"); AddFont(Resources, @"Fonts/Venera/Venera-Bold"); AddFont(Resources, @"Fonts/Venera/Venera-Black"); + + Fonts.AddStore(new HexaconsIcons.HexaconsStore(Textures)); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index d3fadb452b..2a3f4365cb 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD this.getLookup = getLookup; } - public ITexturedCharacterGlyph? Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string? fontName, char character) { string lookup = getLookup(character); var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}"); diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 041a32e8de..8aefa50252 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -63,7 +63,7 @@ namespace osu.Game.Skinning this.maxSize = maxSize; } - public ITexturedCharacterGlyph? Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string? fontName, char character) { string lookup = getLookupName(character); From 340227a06d65c6b8bb1edf2e4834ab04d7f809ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 13:16:04 +0900 Subject: [PATCH 08/51] Replace all hexacon lookups with strongly typed properties --- .../UserInterface/TestSceneOverlayHeader.cs | 3 +- osu.Game/Graphics/HexaconsIcons.cs | 27 +++++++++-- .../BeatmapListing/BeatmapListingHeader.cs | 3 +- .../Overlays/BeatmapSet/BeatmapSetHeader.cs | 3 +- .../Overlays/Changelog/ChangelogHeader.cs | 3 +- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 4 +- osu.Game/Overlays/ChatOverlay.cs | 4 +- .../Dashboard/DashboardOverlayHeader.cs | 3 +- osu.Game/Overlays/FullscreenOverlay.cs | 3 +- osu.Game/Overlays/INamedOverlayComponent.cs | 3 +- osu.Game/Overlays/News/NewsHeader.cs | 3 +- osu.Game/Overlays/NotificationOverlay.cs | 4 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Overlays/OverlayTitle.cs | 45 ++++++------------- osu.Game/Overlays/Profile/ProfileHeader.cs | 3 +- .../Rankings/RankingsOverlayHeader.cs | 3 +- osu.Game/Overlays/SettingsOverlay.cs | 4 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 12 ++--- .../Overlays/Toolbar/ToolbarHomeButton.cs | 3 +- .../Toolbar/ToolbarOverlayToggleButton.cs | 2 +- osu.Game/Overlays/Wiki/WikiHeader.cs | 3 +- .../Edit/Components/Menus/EditorMenuBar.cs | 4 +- .../Screens/Edit/Setup/SetupScreenHeader.cs | 3 +- 23 files changed, 82 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index a927b0931b..55a04b129c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -153,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface public TestTitle() { Title = "title"; - IconTexture = "Icons/changelog"; + Icon = HexaconsIcons.Devtools; } } } diff --git a/osu.Game/Graphics/HexaconsIcons.cs b/osu.Game/Graphics/HexaconsIcons.cs index 9b0fb30963..3eee5d7197 100644 --- a/osu.Game/Graphics/HexaconsIcons.cs +++ b/osu.Game/Graphics/HexaconsIcons.cs @@ -16,12 +16,31 @@ namespace osu.Game.Graphics { public const string FONT_NAME = "Icons/Hexacons"; + public static IconUsage BeatmapPacks => get(HexaconsMapping.beatmap_packs); + public static IconUsage Beatmap => get(HexaconsMapping.beatmap); + public static IconUsage Calendar => get(HexaconsMapping.calendar); + public static IconUsage Chart => get(HexaconsMapping.chart); + public static IconUsage Community => get(HexaconsMapping.community); + public static IconUsage Contests => get(HexaconsMapping.contests); + public static IconUsage Devtools => get(HexaconsMapping.devtools); + public static IconUsage Download => get(HexaconsMapping.download); public static IconUsage Editor => get(HexaconsMapping.editor); + public static IconUsage FeaturedArtist => get(HexaconsMapping.featured_artist); + public static IconUsage Home => get(HexaconsMapping.home); + public static IconUsage Messaging => get(HexaconsMapping.messaging); + public static IconUsage Music => get(HexaconsMapping.music); + public static IconUsage News => get(HexaconsMapping.news); + public static IconUsage Notification => get(HexaconsMapping.notification); + public static IconUsage Profile => get(HexaconsMapping.profile); + public static IconUsage Rankings => get(HexaconsMapping.rankings); + public static IconUsage Search => get(HexaconsMapping.search); + public static IconUsage Settings => get(HexaconsMapping.settings); + public static IconUsage Social => get(HexaconsMapping.social); + public static IconUsage Store => get(HexaconsMapping.store); + public static IconUsage Tournament => get(HexaconsMapping.tournament); + public static IconUsage Wiki => get(HexaconsMapping.wiki); - private static IconUsage get(HexaconsMapping icon) - { - return new IconUsage((char)icon, FONT_NAME); - } + private static IconUsage get(HexaconsMapping icon) => new IconUsage((char)icon, FONT_NAME); // Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility. // Names should match filenames in resources. diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 3336c383ff..27fab82bf3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -23,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing { Title = PageTitleStrings.MainBeatmapsetsControllerIndex; Description = NamedOverlayComponentStrings.BeatmapListingDescription; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = HexaconsIcons.Beatmap; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 858742648c..eced27f35e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -59,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = PageTitleStrings.MainBeatmapsetsControllerShow; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = HexaconsIcons.Beatmap; } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index e9be67e977..61ea9dc4db 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -123,7 +124,7 @@ namespace osu.Game.Overlays.Changelog { Title = PageTitleStrings.MainChangelogControllerDefault; Description = NamedOverlayComponentStrings.ChangelogDescription; - IconTexture = "Icons/Hexacons/devtools"; + Icon = HexaconsIcons.Devtools; } } } diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 0410174dc1..c6f63a72b9 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -45,11 +45,11 @@ namespace osu.Game.Overlays.Chat { new Drawable[] { - new Sprite + new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = textures.Get("Icons/Hexacons/messaging"), + Icon = HexaconsIcons.Social, Size = new Vector2(18), }, // Placeholder text diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 724f77ad71..5cf2ac6c86 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -29,7 +31,7 @@ namespace osu.Game.Overlays { public partial class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler { - public string IconTexture => "Icons/Hexacons/messaging"; + public IconUsage Icon => HexaconsIcons.Messaging; public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Description => ChatStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 0f4697e33c..b9d869c2ec 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard { Title = PageTitleStrings.MainHomeControllerIndex; Description = NamedOverlayComponentStrings.DashboardDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = HexaconsIcons.Social; } } } diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 6ee045c492..6ddf1eecf0 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays public abstract partial class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent where T : OverlayHeader { - public virtual string IconTexture => Header.Title.IconTexture; + public virtual IconUsage Icon => Header.Title.Icon; public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Description => Header.Title.Description; diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs index 65664b12e7..ef3c029aac 100644 --- a/osu.Game/Overlays/INamedOverlayComponent.cs +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -1,13 +1,14 @@ // 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.Graphics.Sprites; using osu.Framework.Localisation; namespace osu.Game.Overlays { public interface INamedOverlayComponent { - string IconTexture { get; } + IconUsage Icon { get; } LocalisableString Title { get; } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 44e2f6a8cb..f237ed66f2 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -7,6 +7,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.News { Title = PageTitleStrings.MainNewsControllerDefault; Description = NamedOverlayComponentStrings.NewsDescription; - IconTexture = "Icons/Hexacons/news"; + Icon = HexaconsIcons.News; } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 81233b4343..c3ddb228ea 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,9 +13,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; @@ -27,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, INotificationOverlay { - public string IconTexture => "Icons/Hexacons/notification"; + public IconUsage Icon => HexaconsIcons.Notification; public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Description => NotificationsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 5bbf18a959..425ff0935d 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/music"; + public IconUsage Icon => HexaconsIcons.Music; public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 1d207e5f7d..a2ff7032b5 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -1,13 +1,9 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -20,7 +16,7 @@ namespace osu.Game.Overlays public const float ICON_SIZE = 30; private readonly OsuSpriteText titleText; - private readonly Container icon; + private readonly Container iconContainer; private LocalisableString title; @@ -32,12 +28,20 @@ namespace osu.Game.Overlays public LocalisableString Description { get; protected set; } - private string iconTexture; + private IconUsage icon; - public string IconTexture + public IconUsage Icon { - get => iconTexture; - protected set => icon.Child = new OverlayTitleIcon(iconTexture = value); + get => icon; + protected set => iconContainer.Child = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + + Icon = icon = value, + }; } protected OverlayTitle() @@ -51,7 +55,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Horizontal, Children = new Drawable[] { - icon = new Container + iconContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -68,26 +72,5 @@ namespace osu.Game.Overlays } }; } - - private partial class OverlayTitleIcon : Sprite - { - private readonly string textureName; - - public OverlayTitleIcon(string textureName) - { - this.textureName = textureName; - - RelativeSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - FillMode = FillMode.Fit; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Texture = textures.Get(textureName); - } - } } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 80d48ae09e..78343d08f1 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Overlays.Profile.Header; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = PageTitleStrings.MainUsersControllerDefault; - IconTexture = "Icons/Hexacons/profile"; + Icon = HexaconsIcons.Profile; } } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 44f278a237..63128fb73d 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Rulesets; using osu.Game.Users; @@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Rankings { Title = PageTitleStrings.MainRankingControllerDefault; Description = NamedOverlayComponentStrings.RankingsDescription; - IconTexture = "Icons/Hexacons/rankings"; + Icon = HexaconsIcons.Rankings; } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 291281124c..746d451343 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -12,14 +12,16 @@ using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Graphics; using System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; namespace osu.Game.Overlays { public partial class SettingsOverlay : SettingsPanel, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/settings"; + public IconUsage Icon => HexaconsIcons.Settings; public LocalisableString Title => SettingsStrings.HeaderTitle; public LocalisableString Description => SettingsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index e181322dda..08bcb6bd8a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -10,12 +10,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Database; using osu.Framework.Localisation; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -36,16 +35,13 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } - [Resolved] - private TextureStore textures { get; set; } = null!; - [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; - public void SetIcon(string texture) => - SetIcon(new Sprite + public void SetIcon(IconUsage icon) => + SetIcon(new SpriteIcon { - Texture = textures.Get(texture), + Icon = icon, }); public LocalisableString Text diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index ba2c8282c5..ded0229d67 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = ToolbarStrings.HomeHeaderTitle; TooltipSub = ToolbarStrings.HomeHeaderDescription; - SetIcon("Icons/Hexacons/home"); + SetIcon(HexaconsIcons.Home); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 7bd48174db..78c976111b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = named.Title; TooltipSub = named.Description; - SetIcon(named.IconTexture); + SetIcon(named.Icon); } } } diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 9317813fc4..9e9e565684 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Wiki { Title = PageTitleStrings.MainWikiControllerDefault; Description = NamedOverlayComponentStrings.WikiDescription; - IconTexture = "Icons/Hexacons/wiki"; + Icon = HexaconsIcons.Wiki; } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index a67375b0a4..5c77672d90 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -46,12 +46,12 @@ namespace osu.Game.Screens.Edit.Components.Menus Padding = new MarginPadding(8), Children = new Drawable[] { - new Sprite + new SpriteIcon { Size = new Vector2(26), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Texture = textures.Get("Icons/Hexacons/editor"), + Icon = HexaconsIcons.Editor, }, text = new TextFlowContainer { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 788beba9d9..93448c4394 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osuTK.Graphics; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Setup { Title = EditorSetupStrings.BeatmapSetup.ToLower(); Description = EditorSetupStrings.BeatmapSetupDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = HexaconsIcons.Social; } } From 5905ca64925f4cf4b3ce7d07974e39a353df18e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 10:56:59 +0900 Subject: [PATCH 09/51] Add second level menu for skin editors --- .../TestSceneBeatmapEditorNavigation.cs | 2 +- .../UserInterface/TestSceneButtonSystem.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 19 +++++++++++++++++-- osu.Game/Screens/Menu/MainMenu.cs | 10 +++++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index b79b61202b..ce266a2d77 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - AddStep("open editor", () => Game.ChildrenOfType().Single().OnEdit.Invoke()); + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddStep("click on file", () => { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index ac811aeb65..1de47aee69 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface break; case Key.E: - buttons.OnEdit = action; + buttons.OnEditBeatmap = action; break; case Key.D: diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a0cf9f5322..79739e4f0c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -40,7 +41,8 @@ namespace osu.Game.Screens.Menu private readonly IBindable isIdle = new BindableBool(); - public Action OnEdit; + public Action OnEditBeatmap; + public Action OnEditSkin; public Action OnExit; public Action OnBeatmapListing; public Action OnSolo; @@ -84,6 +86,7 @@ namespace osu.Game.Screens.Menu private readonly List buttonsTopLevel = new List(); private readonly List buttonsPlay = new List(); + private readonly List buttonsEdit = new List(); private Sample sampleBackToLogo; private Sample sampleLogoSwoosh; @@ -105,6 +108,11 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, + -WEDGE_WIDTH) + { + VisibleState = ButtonSystemState.Edit, + }, backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { @@ -133,14 +141,19 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); + buttonsEdit.Add(new MainMenuButton(CommonStrings.Beatmaps.ToLower(), @"button-default-select", FontAwesome.Solid.User, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(CommonStrings.Skins.ToLower(), @"button-default-select", FontAwesome.Solid.Users, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonArea.AddRange(buttonsPlay); + buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsTopLevel); buttonArea.ForEach(b => @@ -270,6 +283,7 @@ namespace osu.Game.Screens.Menu return true; + case ButtonSystemState.Edit: case ButtonSystemState.Play: StopSamplePlayback(); backButton.TriggerClick(); @@ -414,6 +428,7 @@ namespace osu.Game.Screens.Menu Initial, TopLevel, Play, + Edit, EnteringMode, } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0f73707544..c25e62d69e 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -25,6 +25,7 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; @@ -93,6 +94,9 @@ namespace osu.Game.Screens.Menu private Sample reappearSampleSwoosh; + [Resolved(canBeNull: true)] + private SkinEditorOverlay skinEditor { get; set; } + [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio) { @@ -120,11 +124,15 @@ namespace osu.Game.Screens.Menu { Buttons = new ButtonSystem { - OnEdit = delegate + OnEditBeatmap = () => { Beatmap.SetDefault(); this.Push(new EditorLoader()); }, + OnEditSkin = () => + { + skinEditor?.Show(); + }, OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), From 7e59a1d0be3eb3aae50f17e1a361b6a357286194 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:00:22 +0900 Subject: [PATCH 10/51] Apply NRT to `ButtonSystem` --- .../Editing/TestSceneOpenEditorTimestamp.cs | 2 +- .../TestSceneBeatmapEditorNavigation.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 52 +++++++++---------- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 2c8655a5f5..1a754d5145 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Editing () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(1)); - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); addStepClickLink("00:00:000 (1)", waitForSeek: false); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index ce266a2d77..9930349b1b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap.Invoke()); + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap?.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddStep("click on file", () => { diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 79739e4f0c..c4c7599fd7 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -1,12 +1,9 @@ // 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 System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -37,24 +34,23 @@ namespace osu.Game.Screens.Menu { public partial class ButtonSystem : Container, IStateful, IKeyBindingHandler { - public event Action StateChanged; - - private readonly IBindable isIdle = new BindableBool(); - - public Action OnEditBeatmap; - public Action OnEditSkin; - public Action OnExit; - public Action OnBeatmapListing; - public Action OnSolo; - public Action OnSettings; - public Action OnMultiplayer; - public Action OnPlaylists; - public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; - [CanBeNull] - private OsuLogo logo; + public event Action? StateChanged; + + public Action? OnEditBeatmap; + public Action? OnEditSkin; + public Action? OnExit; + public Action? OnBeatmapListing; + public Action? OnSolo; + public Action? OnSettings; + public Action? OnMultiplayer; + public Action? OnPlaylists; + + private readonly IBindable isIdle = new BindableBool(); + + private OsuLogo? logo; /// /// Assign the that this ButtonSystem should manage the position of. @@ -88,8 +84,8 @@ namespace osu.Game.Screens.Menu private readonly List buttonsPlay = new List(); private readonly List buttonsEdit = new List(); - private Sample sampleBackToLogo; - private Sample sampleLogoSwoosh; + private Sample? sampleBackToLogo; + private Sample? sampleLogoSwoosh; private readonly LogoTrackingContainer logoTrackingContainer; @@ -124,17 +120,17 @@ namespace osu.Game.Screens.Menu buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade; } - [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private OsuGame? game { get; set; } - [Resolved(CanBeNull = true)] - private LoginOverlay loginOverlay { get; set; } + [Resolved] + private LoginOverlay? loginOverlay { get; set; } - [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) + [BackgroundDependencyLoader] + private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); @@ -354,7 +350,7 @@ namespace osu.Game.Screens.Menu } } - private ScheduledDelegate logoDelayedAction; + private ScheduledDelegate? logoDelayedAction; private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial) { From 8ad414488a630787453fe953bb1c70d327daa992 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:02:30 +0900 Subject: [PATCH 11/51] Play out previous transforms immediately to avoid flow issues with multiple sub menus --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index c4c7599fd7..a954fb50b7 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -338,6 +338,8 @@ namespace osu.Game.Screens.Menu Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); + buttonArea.FinishTransforms(true); + using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0)) { buttonArea.ButtonSystemState = state; From 1d1b3ca9826127ad986bfd3afc35810622602406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:05:57 +0900 Subject: [PATCH 12/51] Apply NRT to `MainMenuButton` --- osu.Game/Screens/Menu/MainMenuButton.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 63fc34b4fb..daf8451899 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.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 System.Linq; using osu.Framework; @@ -33,7 +31,7 @@ namespace osu.Game.Screens.Menu /// public partial class MainMenuButton : BeatSyncedContainer, IStateful { - public event Action StateChanged; + public event Action? StateChanged; public readonly Key[] TriggerKeys; @@ -48,14 +46,14 @@ namespace osu.Game.Screens.Menu /// public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; - private readonly Action clickAction; - private Sample sampleClick; - private Sample sampleHover; - private SampleChannel sampleChannel; + private readonly Action? clickAction; + private Sample? sampleClick; + private Sample? sampleHover; + private SampleChannel? sampleChannel; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, params Key[] triggerKeys) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; From a069a673fa1866e06451ba620daa2191df0e8403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:15:18 +0900 Subject: [PATCH 13/51] Allow buttons to be displayed on more than one state (and share the back button) --- osu.Game/Screens/Menu/ButtonSystem.cs | 8 ++------ osu.Game/Screens/Menu/MainMenuButton.cs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a954fb50b7..995cdaf450 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -107,12 +107,8 @@ namespace osu.Game.Screens.Menu backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { - VisibleState = ButtonSystemState.Edit, - }, - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, - -WEDGE_WIDTH) - { - VisibleState = ButtonSystemState.Play, + VisibleStateMin = ButtonSystemState.Play, + VisibleStateMax = ButtonSystemState.Edit, }, logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f)) }); diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index daf8451899..bc638b44ac 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -42,9 +42,19 @@ namespace osu.Game.Screens.Menu private readonly string sampleName; /// - /// The menu state for which we are visible for. + /// The menu state for which we are visible for (assuming only one). /// - public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleState + { + set + { + VisibleStateMin = value; + VisibleStateMax = value; + } + } + + public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel; private readonly Action? clickAction; private Sample? sampleClick; @@ -313,9 +323,9 @@ namespace osu.Game.Screens.Menu break; default: - if (value == VisibleState) + if (value <= VisibleStateMax && value >= VisibleStateMin) State = ButtonState.Expanded; - else if (value < VisibleState) + else if (value < VisibleStateMin) State = ButtonState.Contracted; else State = ButtonState.Exploded; From a02b6a5bcb23af26a6fee502a9423b517f75b112 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:28:06 +0900 Subject: [PATCH 14/51] Update menu key shortcut test coverage --- .../UserInterface/TestSceneButtonSystem.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index 1de47aee69..8f72be37df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -67,14 +67,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Enter mode", performEnterMode); } - [TestCase(Key.P, true)] - [TestCase(Key.M, true)] - [TestCase(Key.L, true)] - [TestCase(Key.E, false)] - [TestCase(Key.D, false)] - [TestCase(Key.Q, false)] - [TestCase(Key.O, false)] - public void TestShortcutKeys(Key key, bool entersPlay) + [TestCase(Key.P, Key.P)] + [TestCase(Key.M, Key.P)] + [TestCase(Key.L, Key.P)] + [TestCase(Key.B, Key.E)] + [TestCase(Key.S, Key.E)] + [TestCase(Key.D, null)] + [TestCase(Key.Q, null)] + [TestCase(Key.O, null)] + public void TestShortcutKeys(Key key, Key? subMenuEnterKey) { int activationCount = -1; AddStep("set up action", () => @@ -96,10 +97,14 @@ namespace osu.Game.Tests.Visual.UserInterface buttons.OnPlaylists = action; break; - case Key.E: + case Key.B: buttons.OnEditBeatmap = action; break; + case Key.S: + buttons.OnEditSkin = action; + break; + case Key.D: buttons.OnBeatmapListing = action; break; @@ -117,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"press {key}", () => InputManager.Key(key)); AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); - if (entersPlay) + if (subMenuEnterKey != null) { - AddStep("press P", () => InputManager.Key(Key.P)); - AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); + AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value)); + AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel); } AddStep($"press {key}", () => InputManager.Key(key)); From b8179aa875dcb27746bf1e9f236117c3f9bdb30c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 13:23:35 +0900 Subject: [PATCH 15/51] Use better(?) icons and full strings --- osu.Game/Localisation/EditorStrings.cs | 5 +++++ osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index b20b5bc65a..6ad12f54df 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -9,6 +9,11 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.Editor"; + /// + /// "Beatmap editor" + /// + public static LocalisableString BeatmapEditor => new TranslatableString(getKey(@"beatmap_editor"), @"Beatmap editor"); + /// /// "Waveform opacity" /// diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 995cdaf450..8173375c29 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -133,8 +133,8 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(CommonStrings.Beatmaps.ToLower(), @"button-default-select", FontAwesome.Solid.User, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); - buttonsEdit.Add(new MainMenuButton(CommonStrings.Skins.ToLower(), @"button-default-select", FontAwesome.Solid.Users, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", HexaconsIcons.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", HexaconsIcons.Editor, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); From 62a04a93c837db0aef81557585120877f513660c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Nov 2023 16:22:34 +0900 Subject: [PATCH 16/51] Implement legacy catch health processor --- .../Scoring/CatchHealthProcessor.cs | 15 ++ .../Scoring/LegacyCatchHealthProcessor.cs | 237 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs create mode 100644 osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs new file mode 100644 index 0000000000..7e8162bdfa --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + public partial class CatchHealthProcessor : DrainingHealthProcessor + { + public CatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs new file mode 100644 index 0000000000..ef13ba222c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs @@ -0,0 +1,237 @@ +// 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.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + /// + /// Reference implementation for osu!stable's HP drain. + /// Cannot be used for gameplay. + /// + public partial class LegacyCatchHealthProcessor : DrainingHealthProcessor + { + private const double hp_bar_maximum = 200; + private const double hp_combo_geki = 14; + private const double hp_hit_300 = 6; + private const double hp_slider_tick = 3; + + public Action? OnIterationFail; + public Action? OnIterationSuccess; + public bool ApplyComboEndBonus { get; set; } = true; + + private double lowestHpEver; + private double lowestHpEnd; + private double lowestHpComboEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + private double hpMultiplierComboEnd; + + public LegacyCatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); + lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void ApplyResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void RevertResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + hpMultiplierComboEnd = 1; + + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.05; + double currentHp; + double currentHpUncapped; + + List<(HitObject hitObject, bool newCombo)> allObjects = enumerateHitObjects(Beatmap).Where(h => h.hitObject is Fruit || h.hitObject is Droplet || h.hitObject is Banana).ToList(); + + do + { + currentHp = hp_bar_maximum; + currentHpUncapped = hp_bar_maximum; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int comboTooLowCount = 0; + string failReason = string.Empty; + + for (int i = 0; i < allObjects.Count; i++) + { + HitObject h = allObjects[i].hitObject; + + // Find active break (between current and lastTime) + double localLastTime = lastTime; + double breakTime = 0; + + // Subtract any break time from the duration since the last object + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; + break; + } + + switch (h) + { + case Fruit: + if (ApplyComboEndBonus && (i == allObjects.Count - 1 || allObjects[i + 1].newCombo)) + { + increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); + + if (currentHp < lowestHpComboEnd) + { + if (++comboTooLowCount > 2) + { + hpMultiplierComboEnd *= 1.07; + hpMultiplierNormal *= 1.03; + fail = true; + failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; + } + } + } + else + increaseHp(hpMultiplierNormal * hp_hit_300); + + break; + + case Banana: + increaseHp(hpMultiplierNormal / 2); + break; + + case TinyDroplet: + increaseHp(hpMultiplierNormal * hp_slider_tick * 0.1); + break; + + case Droplet: + increaseHp(hpMultiplierNormal * hp_slider_tick); + break; + } + + if (fail) + break; + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierComboEnd *= 1.01; + hpMultiplierNormal *= 1.01; + failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; + } + + double recovery = (currentHpUncapped - hp_bar_maximum) / allObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierComboEnd *= 1.02; + hpMultiplierNormal *= 1.01; + failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; + } + + if (fail) + { + OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); + continue; + } + + OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); + return testDrop / hp_bar_maximum; + } while (true); + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(double amount) + { + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); + } + } + + private IEnumerable<(HitObject hitObject, bool newCombo)> enumerateHitObjects(IBeatmap beatmap) + { + return enumerateRecursively(beatmap.HitObjects); + + static IEnumerable<(HitObject hitObject, bool newCombo)> enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + // The combo end will either be attached to the hitobject itself if it has no children, or the very first child if it has children. + bool newCombo = (hitObject as IHasComboInformation)?.NewCombo ?? false; + + foreach ((HitObject nested, bool _) in enumerateRecursively(hitObject.NestedHitObjects)) + { + yield return (nested, newCombo); + + // Since the combo was attached to the first child, don't attach it to any other child or the parenting hitobject itself. + newCombo = false; + } + + yield return (hitObject, newCombo); + } + } + } + } +} From acf3de5e25f343f492bbf33135bb2160126a94e6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:22:46 +0900 Subject: [PATCH 17/51] Add CatchHealthProcessor, following legacy calculations --- .../Scoring/CatchHealthProcessor.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 7e8162bdfa..1f6a696f98 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -1,15 +1,175 @@ // 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.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public partial class CatchHealthProcessor : DrainingHealthProcessor { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + public CatchHealthProcessor(double drainStartTime) : base(drainStartTime) { } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + while (true) + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + + List allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList(); + + for (int i = 0; i < allObjects.Count; i++) + { + HitObject h = allObjects[i]; + + double localLastTime = lastTime; + double breakTime = 0; + + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); + } + + double recovery = (currentHpUncapped - 1) / allObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); + } + + if (!fail) + { + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; + } + } + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = healthIncreaseFor(hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type); + + private double healthIncreaseFor(HitResult result) + { + double increase = 0; + + switch (result) + { + case HitResult.SmallTickMiss: + return 0; + + case HitResult.LargeTickMiss: + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + increase = 0.0015; + break; + + case HitResult.LargeTickHit: + increase = 0.015; + break; + + case HitResult.Great: + increase = 0.03; + break; + + case HitResult.LargeBonus: + increase = 0.0025; + break; + } + + return hpMultiplierNormal * increase; + } } } From 4ba6450c7703bbd44e97a5b3180a5afc4a5115ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:32:14 +0900 Subject: [PATCH 18/51] Use better break calculation --- .../Scoring/CatchHealthProcessor.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 1f6a696f98..6d831ad223 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -65,22 +64,16 @@ namespace osu.Game.Rulesets.Catch.Scoring { HitObject h = allObjects[i]; - double localLastTime = lastTime; - double breakTime = 0; - - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; } - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + reduceHp(testDrop * (h.StartTime - lastTime)); lastTime = h.GetEndTime(); From bb662676347c79343da3ba480693ff5c272b6878 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:46:41 +0900 Subject: [PATCH 19/51] Actually use CatchHealthProcessor for the ruleset --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9ceb78893e..013a709663 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); From 4c7d2bb0fbae426e533572ad905531677f3f27fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:14:25 +0900 Subject: [PATCH 20/51] Apply NRT to `SpectatorScreen` --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 31 +++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 48b5c210b8..f9d5355663 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -1,13 +1,10 @@ // 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 System.Collections.Generic; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -33,22 +30,27 @@ namespace osu.Game.Screens.Spectate private readonly List users = new List(); [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); + private IDisposable? realmSubscription; + /// /// Creates a new . /// @@ -58,11 +60,6 @@ namespace osu.Game.Screens.Spectate this.users.AddRange(users); } - [Resolved] - private RealmAccess realm { get; set; } - - private IDisposable realmSubscription; - protected override void LoadComplete() { base.LoadComplete(); @@ -90,7 +87,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes) + private void beatmapsChanged(IRealmCollection items, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; @@ -109,7 +106,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { @@ -207,14 +204,14 @@ namespace osu.Game.Screens.Spectate /// /// The user whose state has changed. /// The new state. - protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, SpectatorState spectatorState); /// /// Starts gameplay for a user. /// /// The user to start gameplay for. /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); + protected abstract void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState); /// /// Quits gameplay for a user. @@ -243,7 +240,7 @@ namespace osu.Game.Screens.Spectate { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) { foreach ((int userId, var _) in userMap) spectatorClient.StopWatchingUser(userId); From dabbdf674bc7cab4bbb414ef451c2af158ff8be4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:19:04 +0900 Subject: [PATCH 21/51] Rename `SoloSpectator` to `SoloSpectatorScreen` --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++---- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- .../Play/{SoloSpectator.cs => SoloSpectatorScreen.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Screens/Play/{SoloSpectator.cs => SoloSpectatorScreen.cs} (98%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index ffd034e4d2..db74f7d673 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient; private DependenciesScreen dependenciesScreen; - private SoloSpectator spectatorScreen; + private SoloSpectatorScreen spectatorScreen; private BeatmapSetInfo importedBeatmap; private int importedBeatmapId; @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectatorScreen); start(); waitForPlayer(); @@ -255,7 +255,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectatorScreen); } [Test] @@ -381,7 +381,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); + AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectatorScreen(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 02f0a6e80d..6967a61204 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -212,7 +212,7 @@ namespace osu.Game.Overlays.Dashboard Text = "Spectate", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), + Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))), Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs similarity index 98% rename from osu.Game/Screens/Play/SoloSpectator.cs rename to osu.Game/Screens/Play/SoloSpectatorScreen.cs index f5af2684d3..59b7d87129 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -33,7 +33,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public partial class SoloSpectator : SpectatorScreen, IPreviewTrackOwner + public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { [NotNull] private readonly APIUser targetUser; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play private APIBeatmapSet beatmapSet; - public SoloSpectator([NotNull] APIUser targetUser) + public SoloSpectatorScreen([NotNull] APIUser targetUser) : base(targetUser.Id) { this.targetUser = targetUser; From 335e8efff769c4ccd5cb4b9c66687a1cc16f941d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:56:49 +0900 Subject: [PATCH 22/51] Apply NRT to `SoloSpecatorScreen` --- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 38 ++++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 59b7d87129..7acdc51fb3 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -1,10 +1,7 @@ // 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.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -35,39 +32,38 @@ namespace osu.Game.Screens.Play [Cached(typeof(IPreviewTrackOwner))] public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { - [NotNull] - private readonly APIUser targetUser; + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } = null!; [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private BeatmapModelDownloader beatmapDownloader { get; set; } + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private Container beatmapPanelContainer; - private RoundedButton watchButton; - private SettingsCheckbox automaticDownload; + private Container beatmapPanelContainer = null!; + private RoundedButton watchButton = null!; + private SettingsCheckbox automaticDownload = null!; + + private readonly APIUser targetUser; /// /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private SpectatorGameplayState immediateSpectatorGameplayState; + private SpectatorGameplayState? immediateSpectatorGameplayState; - private GetBeatmapSetRequest onlineBeatmapRequest; + private GetBeatmapSetRequest? onlineBeatmapRequest; - private APIBeatmapSet beatmapSet; + private APIBeatmapSet? beatmapSet; - public SoloSpectatorScreen([NotNull] APIUser targetUser) + public SoloSpectatorScreen(APIUser targetUser) : base(targetUser.Id) { this.targetUser = targetUser; @@ -199,10 +195,12 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); } - private ScheduledDelegate scheduledStart; + private ScheduledDelegate? scheduledStart; - private void scheduleStart(SpectatorGameplayState spectatorGameplayState) + private void scheduleStart(SpectatorGameplayState? spectatorGameplayState) { + Debug.Assert(spectatorGameplayState != null); + // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); scheduledStart = Schedule(() => From e06d79281dc682fadf83cac72bd0bfc2561e3e65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 16:40:22 +0900 Subject: [PATCH 23/51] Update tests to work with nested screen --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index db74f7d673..9a1fd660d5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -100,19 +100,18 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true); + AddUntilStep("wait for player loader", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddUntilStep("queue send frames on player load", () => { - var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer; + var loadingPlayer = this.ChildrenOfType().SingleOrDefault()?.CurrentPlayer; if (loadingPlayer == null) return false; loadingPlayer.OnLoadComplete += _ => - { spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start); - }; + return true; }); @@ -360,12 +359,12 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; - private Player player => Stack.CurrentScreen as Player; + private Player player => this.ChildrenOfType().Single(); private double currentFrameStableTime => player.ChildrenOfType().First().CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); + private void waitForPlayer() => AddUntilStep("wait for player", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); From ed5375536f1cf96446617b68251354a7f458c2c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:14:07 +0900 Subject: [PATCH 24/51] Reduce access to various fields and events in `SpectatorClient` Restore some `virtual` specs to appease Moq --- osu.Game/Online/Spectator/SpectatorClient.cs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 14e137caf1..e7435adf29 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -46,9 +46,9 @@ namespace osu.Game.Online.Spectator public IBindableList PlayingUsers => playingUsers; /// - /// Whether the local user is playing. + /// Whether the spectated user is playing. /// - protected internal bool IsPlaying { get; private set; } + private bool isPlaying { get; set; } /// /// Called whenever new frames arrive from the server. @@ -58,17 +58,17 @@ namespace osu.Game.Online.Spectator /// /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public virtual event Action? OnUserBeganPlaying; + public event Action? OnUserBeganPlaying; /// /// Called whenever a user finishes a play session. /// - public virtual event Action? OnUserFinishedPlaying; + public event Action? OnUserFinishedPlaying; /// /// Called whenever a user-submitted score has been fully processed. /// - public virtual event Action? OnUserScoreProcessed; + public event Action? OnUserScoreProcessed; /// /// A dictionary containing all users currently being watched, with the number of watching components for each user. @@ -114,7 +114,7 @@ namespace osu.Game.Online.Spectator } // re-send state in case it wasn't received - if (IsPlaying) + if (isPlaying) // TODO: this is likely sent out of order after a reconnect scenario. needs further consideration. BeginPlayingInternal(currentScoreToken, currentState); } @@ -179,10 +179,10 @@ namespace osu.Game.Online.Spectator // This schedule is only here to match the one below in `EndPlaying`. Schedule(() => { - if (IsPlaying) + if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); - IsPlaying = true; + isPlaying = true; // transfer state at point of beginning play currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; @@ -202,7 +202,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) => Schedule(() => { - if (!IsPlaying) + if (!isPlaying) { Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored."); return; @@ -224,7 +224,7 @@ namespace osu.Game.Online.Spectator // We probably need to find a better way to handle this... Schedule(() => { - if (!IsPlaying) + if (!isPlaying) return; // Disposal can take some time, leading to EndPlaying potentially being called after a future play session. @@ -235,7 +235,7 @@ namespace osu.Game.Online.Spectator if (pendingFrames.Count > 0) purgePendingFrames(); - IsPlaying = false; + isPlaying = false; currentBeatmap = null; if (state.HasPassed) From 73ad92189b0b8026ec966a8051d72d04637f8ead Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 12:07:01 +0900 Subject: [PATCH 25/51] Add test coverage of remote player quitting immediately quitting locally --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9a1fd660d5..e3a10c655a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -332,6 +332,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + start(); sendFrames(); waitForPlayer(); From b024065857bbfbf2953153d3ccc2ee90171f8e52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 14:21:52 +0900 Subject: [PATCH 26/51] Remove implicit schedule of `abstract` methods in `SpectatorScreen` This allows each implementation to have control over scheduling. Without this, the solo implementation would not be able to handle quit events while watching a player, as it would push a child (gameplay) screen to the stack where the `SpectatorScreen` would usually be. --- .../Spectate/MultiSpectatorScreen.cs | 8 +++---- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 22 ++++++++++++------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 9 +++++--- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c1b1127542..eb2980fe1e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => { var playerArea = instances.Single(i => i.UserId == userId); @@ -242,9 +242,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; playerArea.LoadScore(spectatorGameplayState.Score); - } + }); - protected override void QuitGameplay(int userId) + protected override void QuitGameplay(int userId) => Schedule(() => { RemoveUser(userId); @@ -252,7 +252,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); - } + }); public override bool OnBackButton() { diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 7acdc51fb3..770018a0c8 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -164,27 +164,33 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) => Schedule(() => { clearDisplay(); showBeatmapPanel(spectatorState); - } + }); - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => { immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; scheduleStart(spectatorGameplayState); - } + }); protected override void QuitGameplay(int userId) { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - watchButton.Enabled.Value = false; + // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). + this.MakeCurrent(); - clearDisplay(); + Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + watchButton.Enabled.Value = false; + + clearDisplay(); + }); } private void clearDisplay() diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f9d5355663..21f96c83f5 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { case SpectatedUserState.Playing: - Schedule(() => OnNewPlayingUserState(userId, newState)); + OnNewPlayingUserState(userId, newState); startGameplay(userId); break; @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; - Schedule(() => StartGameplay(userId, gameplayState)); + StartGameplay(userId, gameplayState); } /// @@ -196,11 +196,12 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); gameplayStates.Remove(userId); - Schedule(() => QuitGameplay(userId)); + QuitGameplay(userId); } /// /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user whose state has changed. /// The new state. @@ -208,6 +209,7 @@ namespace osu.Game.Screens.Spectate /// /// Starts gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to start gameplay for. /// The gameplay state. @@ -215,6 +217,7 @@ namespace osu.Game.Screens.Spectate /// /// Quits gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to quit gameplay for. protected abstract void QuitGameplay(int userId); From 51e2ce500d67c4f634d75af59b04ba69302ef44f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 12:45:03 +0900 Subject: [PATCH 27/51] Add test coverage of remote user failing immediately failing locally --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e3a10c655a..f5e4c5da51 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -352,6 +352,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed); + start(); sendFrames(); waitForPlayer(); From ca93fdc94b07bd1bd539d72265ea65edfe8fa6a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:53:35 +0900 Subject: [PATCH 28/51] Add visualisation of when a spectated player fails Create a new stack each time for isolation and safety --- .../Spectate/MultiSpectatorScreen.cs | 5 ++++ osu.Game/Screens/Play/SoloSpectatorPlayer.cs | 4 +-- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 14 ++++++++++- osu.Game/Screens/Play/SpectatorPlayer.cs | 12 ++++++++- osu.Game/Screens/Spectate/SpectatorScreen.cs | 25 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index eb2980fe1e..e2159f0e3b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -244,6 +244,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate playerArea.LoadScore(spectatorGameplayState.Score); }); + protected override void FailGameplay(int userId) + { + // We probably want to visualise this in the future. + } + protected override void QuitGameplay(int userId) => Schedule(() => { RemoveUser(userId); diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index c9d1f4acaa..8d25a0148d 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -17,8 +17,8 @@ namespace osu.Game.Screens.Play protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); - public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null) - : base(score, configuration) + public SoloSpectatorPlayer(Score score) + : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.score = score; } diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 770018a0c8..d4a9fc4b30 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -178,6 +178,19 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); }); + protected override void FailGameplay(int userId) + { + if (this.GetChildScreen() is SpectatorPlayer player) + player.AllowFail(); + + Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + clearDisplay(); + }); + } + protected override void QuitGameplay(int userId) { // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). @@ -187,7 +200,6 @@ namespace osu.Game.Screens.Play { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; - watchButton.Enabled.Value = false; clearDisplay(); }); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 30a5ac3741..bc95fa190c 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -25,7 +25,15 @@ namespace osu.Game.Screens.Play private readonly Score score; - protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap + protected override bool CheckModsAllowFailure() + { + if (!allowFail) + return false; + + return base.CheckModsAllowFailure(); + } + + private bool allowFail; protected SpectatorPlayer(Score score, PlayerConfiguration configuration = null) : base(configuration) @@ -123,5 +131,7 @@ namespace osu.Game.Screens.Play if (SpectatorClient != null) SpectatorClient.OnNewFrames -= userSentFrames; } + + public void AllowFail() => allowFail = true; } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 21f96c83f5..c4aef3c878 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -137,6 +137,10 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); break; + case SpectatedUserState.Failed: + failGameplay(userId); + break; + case SpectatedUserState.Quit: quitGameplay(userId); break; @@ -185,6 +189,20 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; } + private void failGameplay(int userId) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.ContainsKey(userId)) + return; + + markReceivedAllFrames(userId); + + gameplayStates.Remove(userId); + FailGameplay(userId); + } + private void quitGameplay(int userId) { if (!userMap.ContainsKey(userId)) @@ -222,6 +240,13 @@ namespace osu.Game.Screens.Spectate /// The user to quit gameplay for. protected abstract void QuitGameplay(int userId); + /// + /// Fails gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. + /// + /// The user to fail gameplay for. + protected abstract void FailGameplay(int userId); + /// /// Stops spectating a user. /// From 8375dd72d6e691ef393aecf1e2c3c418f46cd128 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 10:01:59 +0900 Subject: [PATCH 29/51] Add xmldoc to new `AllowFail` method --- osu.Game/Screens/Play/Player.cs | 7 +++++++ osu.Game/Screens/Play/SpectatorPlayer.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8c7fc551ba..ad725e1a90 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -894,6 +894,13 @@ namespace osu.Game.Screens.Play #region Fail Logic + /// + /// Invoked when gameplay has permanently failed. + /// + protected virtual void OnFail() + { + } + protected FailOverlay FailOverlay { get; private set; } private FailAnimationContainer failAnimationContainer; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index bc95fa190c..d1404ac184 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -68,6 +68,12 @@ namespace osu.Game.Screens.Play }, true); } + /// + /// Should be called when it is apparent that the player being spectated has failed. + /// This will subsequently stop blocking the fail screen from displaying (usually done out of safety). + /// + public void AllowFail() => allowFail = true; + protected override void StartGameplay() { base.StartGameplay(); @@ -131,7 +137,5 @@ namespace osu.Game.Screens.Play if (SpectatorClient != null) SpectatorClient.OnNewFrames -= userSentFrames; } - - public void AllowFail() => allowFail = true; } } From 4ad3cb3b4952aeb09847fe4e20fd581eaf8a52a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 18:20:22 +0900 Subject: [PATCH 30/51] Submit and send failed spectator state more aggressively --- osu.Game/Screens/Play/SubmittingPlayer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 30fecbe149..14aaa8f638 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.Play } AddInternal(new PlayerTouchInputDetector()); + + HealthProcessor.Failed += onFail; } protected override void LoadAsyncComplete() @@ -165,10 +167,21 @@ namespace osu.Game.Screens.Play spectatorClient.BeginPlaying(token, GameplayState, Score); } + private bool onFail() + { + submitFromFailOrQuit(); + return true; + } + public override bool OnExiting(ScreenExitEvent e) { bool exiting = base.OnExiting(e); + submitFromFailOrQuit(); + return exiting; + } + private void submitFromFailOrQuit() + { if (LoadedBeatmapSuccessfully) { Task.Run(async () => @@ -177,8 +190,6 @@ namespace osu.Game.Screens.Play spectatorClient.EndPlaying(GameplayState); }).FireAndForget(); } - - return exiting; } /// From ef5dd245894e3ea4b8658929054b92276b91c4e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 10:28:15 +0900 Subject: [PATCH 31/51] Update failing test coverage and fix `onFail` being called too often --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 1 - osu.Game/Screens/Play/Player.cs | 1 + osu.Game/Screens/Play/SubmittingPlayer.cs | 7 +++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 1a7ea20cc0..f75a2656ef 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -179,7 +179,6 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddStep("exit", () => Player.Exit()); AddUntilStep("wait for submission", () => Player.SubmittedScore != null); AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad725e1a90..b469ab443c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -933,6 +933,7 @@ namespace osu.Game.Screens.Play if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(true); + OnFail(); return true; } diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 14aaa8f638..785164178a 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play } AddInternal(new PlayerTouchInputDetector()); - - HealthProcessor.Failed += onFail; } protected override void LoadAsyncComplete() @@ -167,10 +165,11 @@ namespace osu.Game.Screens.Play spectatorClient.BeginPlaying(token, GameplayState, Score); } - private bool onFail() + protected override void OnFail() { + base.OnFail(); + submitFromFailOrQuit(); - return true; } public override bool OnExiting(ScreenExitEvent e) From d3a55d83c000dd1bf0174a9811585e214d605346 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 11:05:52 +0900 Subject: [PATCH 32/51] Schedule `FailScore` inside `onFail` instead of `onFailComplete` --- osu.Game/Screens/Play/Player.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b469ab443c..ff00b52f71 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -930,10 +930,22 @@ namespace osu.Game.Screens.Play failAnimationContainer.Start(); - if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(true); + // Failures can be triggered either by a judgement, or by a mod. + // + // For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received + // the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above). + // + // A schedule here ensures that any lingering judgements from the current frame are applied before we + // finalise the score as "failed". + Schedule(() => + { + ScoreProcessor.FailScore(Score.ScoreInfo); + OnFail(); + + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) + Restart(true); + }); - OnFail(); return true; } @@ -942,11 +954,6 @@ namespace osu.Game.Screens.Play /// private void onFailComplete() { - // fail completion is a good point to mark a score as failed, - // since the last judgement that caused the fail only applies to score processor after onFail. - // todo: this should probably be handled better. - ScoreProcessor.FailScore(Score.ScoreInfo); - GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; From 7ceb49fbc095ae9b8bd91a6bf4c6c2d8763186f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 14:58:57 +0900 Subject: [PATCH 33/51] Add extra test coverage and handle case where still at loading screen --- .../Visual/Gameplay/TestSceneSpectator.cs | 64 +++++++++++++------ osu.Game/Screens/Play/SoloSpectatorScreen.cs | 33 +++++----- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index f5e4c5da51..1c7ede2b19 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(startTime: gameplay_start); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay return true; }); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); AddAssert("time is greater than seek target", () => currentFrameStableTime, () => Is.GreaterThan(gameplay_start)); @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectatorScreen); start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadSpectatingScreen(); start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); // send enough frames to ensure play won't be paused @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(300); loadSpectatingScreen(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(300); @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); Player lastPlayer = null; AddStep("store first player", () => lastPlayer = player); @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddAssert("player is different", () => lastPlayer != player); } @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); sendFrames(); @@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); @@ -236,14 +236,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); // host starts playing a new session start(); - waitForPlayer(); + waitForPlayerCurrent(); } [Test] @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -309,14 +309,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send passed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Passed)); AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -327,7 +327,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); @@ -336,18 +336,19 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] - public void TestFailedState() + public void TestFailedStateDuringPlay() { loadSpectatingScreen(); start(); sendFrames(); - waitForPlayer(); + + waitForPlayerCurrent(); AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); @@ -356,7 +357,28 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestFailedStateDuringLoading() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + + waitForPlayerLoader(); + + AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + + start(); + sendFrames(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -368,7 +390,9 @@ namespace osu.Game.Tests.Visual.Gameplay private double currentFrameStableTime => player.ChildrenOfType().First().CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + private void waitForPlayerLoader() => AddUntilStep("wait for loading", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + + private void waitForPlayerCurrent() => AddUntilStep("wait for player current", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index d4a9fc4b30..2db751402c 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -180,31 +180,32 @@ namespace osu.Game.Screens.Play protected override void FailGameplay(int userId) { - if (this.GetChildScreen() is SpectatorPlayer player) - player.AllowFail(); - - Schedule(() => + if (this.GetChildScreen() is SpectatorPlayerLoader loader) { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - clearDisplay(); - }); + if (loader.GetChildScreen() is SpectatorPlayer player) + { + player.AllowFail(); + resetStartState(); + } + else + QuitGameplay(userId); + } } protected override void QuitGameplay(int userId) { // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). this.MakeCurrent(); - - Schedule(() => - { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - - clearDisplay(); - }); + resetStartState(); } + private void resetStartState() => Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + clearDisplay(); + }); + private void clearDisplay() { watchButton.Enabled.Value = false; From 289cda71b236d98f622f5c8920ba91dd14bb40b8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 15:06:51 +0900 Subject: [PATCH 34/51] Fix inspections --- osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 8 ++++---- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 4bcd6b100a..459a8b0df5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithFilter([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq"); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelect([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 8173375c29..78eb410a48 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Menu /// Assign the that this ButtonSystem should manage the position of. /// /// The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem. - public void SetOsuLogo(OsuLogo logo) + public void SetOsuLogo(OsuLogo? logo) { this.logo = logo; From c126c46e2d25bcf2b1853ef80b869a230a9bb8ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 15:43:57 +0900 Subject: [PATCH 35/51] Remove legacy implementations (moved to osu-tools) --- .../Scoring/LegacyCatchHealthProcessor.cs | 237 ------------------ .../Scoring/LegacyOsuHealthProcessor.cs | 215 ---------------- 2 files changed, 452 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs delete mode 100644 osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs deleted file mode 100644 index ef13ba222c..0000000000 --- a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs +++ /dev/null @@ -1,237 +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.Beatmaps; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Catch.Scoring -{ - /// - /// Reference implementation for osu!stable's HP drain. - /// Cannot be used for gameplay. - /// - public partial class LegacyCatchHealthProcessor : DrainingHealthProcessor - { - private const double hp_bar_maximum = 200; - private const double hp_combo_geki = 14; - private const double hp_hit_300 = 6; - private const double hp_slider_tick = 3; - - public Action? OnIterationFail; - public Action? OnIterationSuccess; - public bool ApplyComboEndBonus { get; set; } = true; - - private double lowestHpEver; - private double lowestHpEnd; - private double lowestHpComboEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - private double hpMultiplierComboEnd; - - public LegacyCatchHealthProcessor(double drainStartTime) - : base(drainStartTime) - { - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); - lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void ApplyResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); - } - - protected override void RevertResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - hpMultiplierComboEnd = 1; - - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.05; - double currentHp; - double currentHpUncapped; - - List<(HitObject hitObject, bool newCombo)> allObjects = enumerateHitObjects(Beatmap).Where(h => h.hitObject is Fruit || h.hitObject is Droplet || h.hitObject is Banana).ToList(); - - do - { - currentHp = hp_bar_maximum; - currentHpUncapped = hp_bar_maximum; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - int comboTooLowCount = 0; - string failReason = string.Empty; - - for (int i = 0; i < allObjects.Count; i++) - { - HitObject h = allObjects[i].hitObject; - - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) - { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } - } - - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; - break; - } - - switch (h) - { - case Fruit: - if (ApplyComboEndBonus && (i == allObjects.Count - 1 || allObjects[i + 1].newCombo)) - { - increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); - - if (currentHp < lowestHpComboEnd) - { - if (++comboTooLowCount > 2) - { - hpMultiplierComboEnd *= 1.07; - hpMultiplierNormal *= 1.03; - fail = true; - failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; - } - } - } - else - increaseHp(hpMultiplierNormal * hp_hit_300); - - break; - - case Banana: - increaseHp(hpMultiplierNormal / 2); - break; - - case TinyDroplet: - increaseHp(hpMultiplierNormal * hp_slider_tick * 0.1); - break; - - case Droplet: - increaseHp(hpMultiplierNormal * hp_slider_tick); - break; - } - - if (fail) - break; - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierComboEnd *= 1.01; - hpMultiplierNormal *= 1.01; - failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; - } - - double recovery = (currentHpUncapped - hp_bar_maximum) / allObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierComboEnd *= 1.02; - hpMultiplierNormal *= 1.01; - failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; - } - - if (fail) - { - OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); - continue; - } - - OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); - return testDrop / hp_bar_maximum; - } while (true); - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(double amount) - { - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); - } - } - - private IEnumerable<(HitObject hitObject, bool newCombo)> enumerateHitObjects(IBeatmap beatmap) - { - return enumerateRecursively(beatmap.HitObjects); - - static IEnumerable<(HitObject hitObject, bool newCombo)> enumerateRecursively(IEnumerable hitObjects) - { - foreach (var hitObject in hitObjects) - { - // The combo end will either be attached to the hitobject itself if it has no children, or the very first child if it has children. - bool newCombo = (hitObject as IHasComboInformation)?.NewCombo ?? false; - - foreach ((HitObject nested, bool _) in enumerateRecursively(hitObject.NestedHitObjects)) - { - yield return (nested, newCombo); - - // Since the combo was attached to the first child, don't attach it to any other child or the parenting hitobject itself. - newCombo = false; - } - - yield return (hitObject, newCombo); - } - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs deleted file mode 100644 index e92c3c9b97..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ /dev/null @@ -1,215 +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.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Osu.Scoring -{ - /// - /// Reference implementation for osu!stable's HP drain. - /// Cannot be used for gameplay. - /// - public partial class LegacyOsuHealthProcessor : DrainingHealthProcessor - { - private const double hp_bar_maximum = 200; - private const double hp_combo_geki = 14; - private const double hp_hit_300 = 6; - private const double hp_slider_repeat = 4; - private const double hp_slider_tick = 3; - - public Action? OnIterationFail; - public Action? OnIterationSuccess; - public bool ApplyComboEndBonus { get; set; } = true; - - private double lowestHpEver; - private double lowestHpEnd; - private double lowestHpComboEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - private double hpMultiplierComboEnd; - - public LegacyOsuHealthProcessor(double drainStartTime) - : base(drainStartTime) - { - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); - lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void ApplyResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); - } - - protected override void RevertResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - hpMultiplierComboEnd = 1; - - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.05; - double currentHp; - double currentHpUncapped; - - do - { - currentHp = hp_bar_maximum; - currentHpUncapped = hp_bar_maximum; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - int comboTooLowCount = 0; - string failReason = string.Empty; - - for (int i = 0; i < Beatmap.HitObjects.Count; i++) - { - HitObject h = Beatmap.HitObjects[i]; - - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) - { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } - } - - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; - break; - } - - double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); - double hpOverkill = Math.Max(0, hpReduction - currentHp); - reduceHp(hpReduction); - - if (h is Slider slider) - { - for (int j = 0; j < slider.RepeatCount + 2; j++) - increaseHp(hpMultiplierNormal * hp_slider_repeat); - foreach (var _ in slider.NestedHitObjects.OfType()) - increaseHp(hpMultiplierNormal * hp_slider_tick); - } - else if (h is Spinner spinner) - { - foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) - increaseHp(hpMultiplierNormal * 1.7); - } - - if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})"; - break; - } - - if (ApplyComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)) - { - increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); - - if (currentHp < lowestHpComboEnd) - { - if (++comboTooLowCount > 2) - { - hpMultiplierComboEnd *= 1.07; - hpMultiplierNormal *= 1.03; - fail = true; - failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; - break; - } - } - } - else - increaseHp(hpMultiplierNormal * hp_hit_300); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierComboEnd *= 1.01; - hpMultiplierNormal *= 1.01; - failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; - } - - double recovery = (currentHpUncapped - hp_bar_maximum) / Beatmap.HitObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierComboEnd *= 1.02; - hpMultiplierNormal *= 1.01; - failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; - } - - if (fail) - { - OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); - continue; - } - - OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); - return testDrop / hp_bar_maximum; - } while (true); - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(double amount) - { - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); - } - } - } -} From 36b45d34f7415327f80ed1a243a30af73390805f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:39:29 +0900 Subject: [PATCH 36/51] Check drag location on mouse down instead of drag start to avoid lenience issues --- osu.Game/Overlays/ChatOverlay.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 724f77ad71..54d5952bc3 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -251,10 +251,14 @@ namespace osu.Game.Overlays { } + protected override bool OnMouseDown(MouseDownEvent e) + { + isDraggingTopBar = topBar.DragBar.IsHovered; + return base.OnMouseDown(e); + } + protected override bool OnDragStart(DragStartEvent e) { - isDraggingTopBar = topBar.IsHovered; - if (!isDraggingTopBar) return base.OnDragStart(e); From 7600595e5dba09c0cd6b3a034be86b427dfc6a03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:39:54 +0900 Subject: [PATCH 37/51] Add drag bar on chat overlay to better signal resizability --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 80 +++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 0410174dc1..44cb07ca91 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -23,6 +22,8 @@ namespace osu.Game.Overlays.Chat private Color4 backgroundColour; + public Drawable DragBar = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { @@ -50,7 +51,7 @@ namespace osu.Game.Overlays.Chat Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = textures.Get("Icons/Hexacons/messaging"), - Size = new Vector2(18), + Size = new Vector2(24), }, // Placeholder text new OsuSpriteText @@ -64,19 +65,90 @@ namespace osu.Game.Overlays.Chat }, }, }, + DragBar = new DragArea + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Background4, + } }; } protected override bool OnHover(HoverEvent e) { - background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint); + DragBar.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - background.FadeColour(backgroundColour, 300, Easing.OutQuint); + DragBar.FadeOut(100); base.OnHoverLost(e); } + + private partial class DragArea : CompositeDrawable + { + private readonly Circle circle; + + public DragArea() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(150, 7), + Margin = new MarginPadding(12), + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateScale(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateScale(); + base.OnHoverLost(e); + } + + private bool dragging; + + protected override bool OnMouseDown(MouseDownEvent e) + { + dragging = true; + updateScale(); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + dragging = false; + updateScale(); + base.OnMouseUp(e); + } + + private void updateScale() + { + if (dragging || IsHovered) + circle.FadeIn(100); + else + circle.FadeTo(0.6f, 100); + + if (dragging) + circle.ScaleTo(1f, 400, Easing.OutQuint); + else if (IsHovered) + circle.ScaleTo(1.05f, 400, Easing.OutElasticHalf); + else + circle.ScaleTo(1f, 500, Easing.OutQuint); + } + } } } From 59800821da1afe62e520af591b172eb34c80bee2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:44:18 +0900 Subject: [PATCH 38/51] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index ea08992710..ddaa371014 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 53d5d6b010..c11dfd06f3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 3b41480beffb8eda3ea8428f1d1a02dc81cfd4c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:46:02 +0900 Subject: [PATCH 39/51] Always show drag bar on mobile --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 44cb07ca91..807bb26502 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Chat }, DragBar = new DragArea { - Alpha = 0, + Alpha = RuntimeInfo.IsMobile ? 1 : 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = colourProvider.Background4, @@ -77,13 +78,15 @@ namespace osu.Game.Overlays.Chat protected override bool OnHover(HoverEvent e) { - DragBar.FadeIn(100); + if (!RuntimeInfo.IsMobile) + DragBar.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - DragBar.FadeOut(100); + if (!RuntimeInfo.IsMobile) + DragBar.FadeOut(100); base.OnHoverLost(e); } From 290c3d63492315e700ff57d468585c0a8d3bc52a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:46:23 +0900 Subject: [PATCH 40/51] Clean up left-overs --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 807bb26502..330c991ce8 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -13,27 +13,22 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Chat { public partial class ChatOverlayTopBar : Container { - private Box background = null!; - - private Color4 backgroundColour; - public Drawable DragBar = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { - Children = new Drawable[] + Children = new[] { - background = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = backgroundColour = colourProvider.Background3, + Colour = colourProvider.Background3, }, new GridContainer { From 95229cb33607ba8ddc43af8da387830f36f63d48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:19:03 +0900 Subject: [PATCH 41/51] Show gameplay when loading the skin editor from the main menu --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 65 +++++++++++++++++++ .../SkinEditor/SkinEditorSceneLibrary.cs | 32 +-------- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index d1e7b97efc..16fc6b6ec6 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -9,12 +12,21 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -31,12 +43,21 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditor? skinEditor; + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -72,6 +93,9 @@ namespace osu.Game.Overlays.SkinEditor { globallyDisableBeatmapSkinSetting(); + if (lastTargetScreen is MainMenu) + PresentGameplay(); + if (skinEditor != null) { skinEditor.Show(); @@ -105,6 +129,28 @@ namespace osu.Game.Overlays.SkinEditor globallyReenableBeatmapSkinSetting(); } + public void PresentGameplay() + { + performer?.PerformFromScreen(screen => + { + if (screen is Player) + return; + + var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); + + IReadOnlyList usableMods = mods.Value; + + if (replayGeneratingMod != null) + usableMods = usableMods.Append(replayGeneratingMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) + mods.Value = mods.Value.Except(invalid).ToArray(); + + if (replayGeneratingMod != null) + screen.Push(new EndlessPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))); + }, new[] { typeof(Player), typeof(PlaySongSelect) }); + } + protected override void Update() { base.Update(); @@ -222,5 +268,24 @@ namespace osu.Game.Overlays.SkinEditor leasedBeatmapSkins?.Return(); leasedBeatmapSkins = null; } + + private partial class EndlessPlayer : ReplayPlayer + { + public EndlessPlayer(Func, Score> createScore) + : base(createScore, new PlayerConfiguration + { + ShowResults = false, + }) + { + } + + protected override void Update() + { + base.Update(); + + if (GameplayState.HasPassed) + GameplayClockContainer.Seek(0); + } + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 9b021632cf..7fa33ddcf5 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,12 +11,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens; -using osu.Game.Screens.Play; using osu.Game.Screens.Select; -using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -36,10 +29,7 @@ namespace osu.Game.Overlays.SkinEditor private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private Bindable> mods { get; set; } = null!; + private SkinEditorOverlay skinEditorOverlay { get; set; } = null!; public SkinEditorSceneLibrary() { @@ -96,24 +86,7 @@ namespace osu.Game.Overlays.SkinEditor Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Action = () => performer?.PerformFromScreen(screen => - { - if (screen is Player) - return; - - var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); - - IReadOnlyList usableMods = mods.Value; - - if (replayGeneratingMod != null) - usableMods = usableMods.Append(replayGeneratingMod).ToArray(); - - if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) - mods.Value = mods.Value.Except(invalid).ToArray(); - - if (replayGeneratingMod != null) - screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)))); - }, new[] { typeof(Player), typeof(PlaySongSelect) }) + Action = () => skinEditorOverlay.PresentGameplay(), }, } }, @@ -137,5 +110,6 @@ namespace osu.Game.Overlays.SkinEditor Content.CornerRadius = 5; } } + } } From 7153c823e8405713579f1a9d6c72f5ac5c2572b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:34:08 +0900 Subject: [PATCH 42/51] Choose a better beatmap if the intro is still playing Also skip intro time. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 16fc6b6ec6..18d1c4c62b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -52,12 +52,18 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private MusicController music { get; set; } = null!; + [Resolved] private IBindable ruleset { get; set; } = null!; [Resolved] private Bindable> mods { get; set; } = null!; + [Resolved] + private IBindable beatmap { get; set; } = null!; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -133,6 +139,14 @@ namespace osu.Game.Overlays.SkinEditor { performer?.PerformFromScreen(screen => { + // If we're playing the intro, switch away to another beatmap. + if (beatmap.Value.BeatmapSetInfo.Protected) + { + music.NextTrack(); + Schedule(PresentGameplay); + return; + } + if (screen is Player) return; @@ -275,6 +289,7 @@ namespace osu.Game.Overlays.SkinEditor : base(createScore, new PlayerConfiguration { ShowResults = false, + AutomaticallySkipIntro = true, }) { } From 8314f656a3107b27fc82ddbad448de4c70bdf41d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 17:32:18 +0900 Subject: [PATCH 43/51] Encapsulate common HP logic from osu and catch HP calculations --- .../Scoring/CatchHealthProcessor.cs | 121 +------------- .../Scoring/OsuHealthProcessor.cs | 155 ++--------------- .../Scoring/LegacyDrainingHealthProcessor.cs | 158 ++++++++++++++++++ 3 files changed, 179 insertions(+), 255 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 6d831ad223..c3cc488941 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -1,138 +1,27 @@ // 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.Beatmaps; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { - public partial class CatchHealthProcessor : DrainingHealthProcessor + public partial class CatchHealthProcessor : LegacyDrainingHealthProcessor { - public Action? OnIterationFail; - public Action? OnIterationSuccess; - - private double lowestHpEver; - private double lowestHpEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - public CatchHealthProcessor(double drainStartTime) : base(drainStartTime) { } - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + protected override IEnumerable EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana); - base.ApplyBeatmap(beatmap); - } + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty(); - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.00025; - double currentHp; - double currentHpUncapped; - - while (true) - { - currentHp = 1; - currentHpUncapped = 1; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - - List allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList(); - - for (int i = 0; i < allObjects.Count; i++) - { - HitObject h = allObjects[i]; - - while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) - { - // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. - // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, - // but this shouldn't have a noticeable impact in practice. - lastTime = h.StartTime; - currentBreak++; - } - - reduceHp(testDrop * (h.StartTime - lastTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); - break; - } - - increaseHp(h); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); - } - - double recovery = (currentHpUncapped - 1) / allObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); - } - - if (!fail) - { - OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); - return testDrop; - } - } - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(HitObject hitObject) - { - double amount = healthIncreaseFor(hitObject.CreateJudgement().MaxResult); - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); - } - } - - protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type); - - private double healthIncreaseFor(HitResult result) + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { double increase = 0; @@ -162,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Scoring break; } - return hpMultiplierNormal * increase; + return HpMultiplierNormal * increase; } } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 3c124b3162..7025a7be65 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -1,166 +1,43 @@ // 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.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - public partial class OsuHealthProcessor : DrainingHealthProcessor + public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor { - public Action? OnIterationFail; - public Action? OnIterationSuccess; - - private double lowestHpEver; - private double lowestHpEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - public OsuHealthProcessor(double drainStartTime) : base(drainStartTime) { } - public override void ApplyBeatmap(IBeatmap beatmap) + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.00025; - double currentHp; - double currentHpUncapped; - - while (true) + switch (hitObject) { - currentHp = 1; - currentHpUncapped = 1; + case Slider slider: + foreach (var nested in slider.NestedHitObjects) + yield return nested; - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; + break; - for (int i = 0; i < Beatmap.HitObjects.Count; i++) - { - HitObject h = Beatmap.HitObjects[i]; + case Spinner spinner: + foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + yield return nested; - while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) - { - // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. - // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, - // but this shouldn't have a noticeable impact in practice. - lastTime = h.StartTime; - currentBreak++; - } - - reduceHp(testDrop * (h.StartTime - lastTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); - break; - } - - double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); - double hpOverkill = Math.Max(0, hpReduction - currentHp); - reduceHp(hpReduction); - - switch (h) - { - case Slider slider: - { - foreach (var nested in slider.NestedHitObjects) - increaseHp(nested); - break; - } - - case Spinner spinner: - { - foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) - increaseHp(nested); - break; - } - } - - // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners - // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. - if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); - break; - } - - increaseHp(h); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); - } - - double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); - } - - if (!fail) - { - OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); - return testDrop; - } - } - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(HitObject hitObject) - { - double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + break; } } - protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type); - - private double healthIncreaseFor(HitObject hitObject, HitResult result) + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { double increase = 0; @@ -206,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - return hpMultiplierNormal * increase; + return HpMultiplierNormal * increase; } } } diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs new file mode 100644 index 0000000000..ce2f7d5624 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -0,0 +1,158 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// A that matches legacy drain rate calculations as best as possible. + /// + public abstract partial class LegacyDrainingHealthProcessor : DrainingHealthProcessor + { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + + protected double HpMultiplierNormal { get; private set; } + + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + + protected LegacyDrainingHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + HpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + while (true) + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int topLevelObjectCount = 0; + + foreach (var h in EnumerateTopLevelHitObjects()) + { + topLevelObjectCount++; + + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) + { + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; + } + + reduceHp(testDrop * (h.StartTime - lastTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); + break; + } + + double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); + double hpOverkill = Math.Max(0, hpReduction - currentHp); + reduceHp(hpReduction); + + foreach (var nested in EnumerateNestedHitObjects(h)) + increaseHp(nested); + + // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners + // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. + if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); + } + + double recovery = (currentHpUncapped - 1) / Math.Max(1, topLevelObjectCount); + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); + } + + if (!fail) + { + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; + } + } + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = GetHealthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected sealed override double GetHealthIncreaseFor(JudgementResult result) => GetHealthIncreaseFor(result.HitObject, result.Type); + + protected abstract IEnumerable EnumerateTopLevelHitObjects(); + + protected abstract IEnumerable EnumerateNestedHitObjects(HitObject hitObject); + + protected abstract double GetHealthIncreaseFor(HitObject hitObject, HitResult result); + } +} From a44edfdeddadaac693c63312fb6d3cc14593ee0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 19:37:57 +0900 Subject: [PATCH 44/51] Fix incorrect sample for top level edit button --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 78eb410a48..259efad8b3 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Menu buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) From 901561533600690974d0c89756cc850dca5ec7d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 19:42:30 +0900 Subject: [PATCH 45/51] Update test to work with new drag bar location --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 55e6b54af7..8a2a66f60f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -180,11 +180,8 @@ namespace osu.Game.Tests.Visual.Online }); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); - AddStep("Click top bar", () => - { - InputManager.MoveMouseTo(chatOverlayTopBar); - InputManager.PressButton(MouseButton.Left); - }); + AddStep("Move mouse to drag bar", () => InputManager.MoveMouseTo(chatOverlayTopBar.DragBar)); + AddStep("Click drag bar", () => InputManager.PressButton(MouseButton.Left)); AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("Store new height", () => newHeight = chatOverlay.Height); From a6cf1e5d2eabb3cd5f32da0b213e20c1ab601a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Nov 2023 00:53:25 +0900 Subject: [PATCH 46/51] Make osu! logo do something when in edit submenu --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 259efad8b3..ca5bef985e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -311,6 +311,10 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Play: buttonsPlay.First().TriggerClick(); return false; + + case ButtonSystemState.Edit: + buttonsEdit.First().TriggerClick(); + return false; } } From 6f66819e5152de82d807522089465b79a82b3850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 10:44:50 +0900 Subject: [PATCH 47/51] Privatise setter --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 1cea198300..84fd342493 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Chat { public partial class ChatOverlayTopBar : Container { - public Drawable DragBar = null!; + public Drawable DragBar { get; private set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) From 7f788058cdadf4f8196b4095afdf2c265b81edab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 11:35:10 +0900 Subject: [PATCH 48/51] Revert incorrect xmldoc change --- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index e7435adf29..47e2dd807a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Spectator public IBindableList PlayingUsers => playingUsers; /// - /// Whether the spectated user is playing. + /// Whether the local user is playing. /// private bool isPlaying { get; set; } From 3f48f4acdff18744317665661eb0119154eceff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 12:06:08 +0900 Subject: [PATCH 49/51] Remove blank line --- osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 7fa33ddcf5..a682285549 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -110,6 +110,5 @@ namespace osu.Game.Overlays.SkinEditor Content.CornerRadius = 5; } } - } } From 7e3bb5f8dba1a3357a6a6729a557d1e0b13714c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 12:09:13 +0900 Subject: [PATCH 50/51] Make skin editor overlay dependency nullable to fix tests --- osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index a682285549..5a283c0e8d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.SkinEditor private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private SkinEditorOverlay skinEditorOverlay { get; set; } = null!; + private SkinEditorOverlay? skinEditorOverlay { get; set; } public SkinEditorSceneLibrary() { @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.SkinEditor Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Action = () => skinEditorOverlay.PresentGameplay(), + Action = () => skinEditorOverlay?.PresentGameplay(), }, } }, From a4be28a2aebf4bf14c72a1aee35d60fad654c88e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:53:06 +0900 Subject: [PATCH 51/51] Don't show buttons on fail overlay when player interaction is disabled --- osu.Game/Screens/Play/FailOverlay.cs | 15 +++++++++++++-- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index abfc401998..210ae5ceb6 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -26,11 +26,22 @@ namespace osu.Game.Screens.Play public override LocalisableString Header => GameplayMenuOverlayStrings.FailedHeader; + private readonly bool showButtons; + + public FailOverlay(bool showButtons = true) + { + this.showButtons = showButtons; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { - AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); - AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + if (showButtons) + { + AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); + AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + } + // from #10339 maybe this is a better visual effect Add(new Container { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ff00b52f71..1c97efcff7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -267,7 +267,7 @@ namespace osu.Game.Screens.Play createGameplayComponents(Beatmap.Value) } }, - FailOverlay = new FailOverlay + FailOverlay = new FailOverlay(Configuration.AllowUserInteraction) { SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false), OnRetry = () => Restart(),