diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs index 03ee7c9204..63c481a623 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 2baa7ee0e0..5babdb47ff 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index a2308e6dfc..5d64ca832a 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs index b45505678c..5beb6616a7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index e839d2657c..6796a8962b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index a2308e6dfc..5d64ca832a 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf index 503e5935f5..606988ccdf 100644 --- a/osu.Desktop.slnf +++ b/osu.Desktop.slnf @@ -16,15 +16,14 @@ "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament\\osu.Game.Tournament.csproj", "osu.Game\\osu.Game.csproj", - - "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", - "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", + "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj" + "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj" ] } -} +} \ No newline at end of file diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6b95a82703..a7453dc0e0 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -102,7 +102,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null })) { if (!host.IsPrimaryInstance) { diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index f37cfdc5f1..d6a11fa924 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,9 +23,9 @@ - + - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 5de21a68d0..47c93fbd02 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -8,7 +8,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index c45c85833c..0a77845343 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 161a59c5fd..12a4182bf1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); } - public override ScoreRank RankFromAccuracy(double accuracy) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs index ab9f57ecc3..a5c18babe2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -1,10 +1,8 @@ // 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 NUnit.Framework; -using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -16,37 +14,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Test] public void TestMinor() { - AddStep("Create barlines", () => recreate()); + AddStep("Create barlines", recreate); } - private void recreate(Func>? createBarLines = null) + private void recreate() { var stageDefinitions = new List { new StageDefinition(4), }; - SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s => + SetContents(_ => { - if (createBarLines != null) + var maniaPlayfield = new ManiaPlayfield(stageDefinitions); + + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => { - var barLines = createBarLines(); - - foreach (var b in barLines) - s.Add(b); - - return; - } - - for (int i = 0; i < 64; i++) - { - s.Add(new BarLine + for (int i = 0; i < 64; i++) { - StartTime = Time.Current + i * 500, - Major = i % 4 == 0, - }); - } - })); + maniaPlayfield.Add(new BarLine + { + StartTime = Time.Current + i * 500, + Major = i % 4 == 0, + }); + } + }); + + return maniaPlayfield; + }); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index b991db408c..877b9c3ea4 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 874130233a..5f5596cbb3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } = null!; - private readonly List> pools; + private readonly List> pools = new List>(); - public TestSceneDrawableJudgement() + [TestCaseSource(nameof(validResults))] + public void Test(HitResult result) { - pools = new List>(); - - foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) - showResult(result); + showResult(result); } + private static IEnumerable validResults => Enum.GetValues().Skip(1); + [Test] public void TestHitLightingDisabled() { @@ -72,32 +72,33 @@ namespace osu.Game.Rulesets.Osu.Tests pools.Add(pool = new DrawablePool(1)); else { - pool = pools[poolIndex]; - // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. + pool = pools[poolIndex]; ((Container)pool.Parent!).Clear(false); } var container = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject - { - StartTime = Time.Current - }, new Judgement()) - { - Type = result, - }, null)).With(j => - { - j.Anchor = Anchor.Centre; - j.Origin = Anchor.Centre; - }) - } + Child = pool, }; + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => + { + container.Add(pool.Get(j => j.Apply(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) + { + Type = result, + }, null)).With(j => + { + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + })); + }); + poolIndex++; return container; }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index f41dd913ab..380a2087ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests { DrawableSlider dho = null; - AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider + AddStep("create slider", () => Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddWaitStep("wait for progression", 1); - AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider + AddStep("apply new slider", () => dho.Apply(applyDefaults(new Slider { Position = new Vector2(256, 192), ComboIndex = 1, @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests Child = new SkinProvidingContainer(provider) { RelativeSizeAxes = Axes.Both, - Child = dho = new DrawableSlider(prepareObject(new Slider + Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, @@ -97,7 +97,38 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red); } - private Slider prepareObject(Slider slider) + [Test] + public void TestIncreaseRepeatCount() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + Child = dho = new DrawableSlider(applyDefaults(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })); + }); + + AddStep("increase repeat count", () => + { + dho.HitObject.RepeatCount++; + applyDefaults(dho.HitObject); + }); + + AddAssert("repeat got custom anchor", () => + dho.ChildrenOfType().Single().RelativeAnchorPosition == Vector2.Divide(dho.SliderBody!.PathOffset, dho.DrawSize)); + } + + private Slider applyDefaults(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return slider; diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index ea033cda45..9c248abd66 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index ed4c02a4a9..baec200107 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container repeatContainer; private PausableSkinnableSound slidingSample; - private readonly LayoutValue drawSizeLayout; + private readonly LayoutValue relativeAnchorPositionLayout; public DrawableSlider() : this(null) @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AlwaysPresent = true, Alpha = 0 }; - AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); + AddLayout(relativeAnchorPositionLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); } [BackgroundDependencyLoader] @@ -190,6 +190,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables repeatContainer.Add(repeat); break; } + + relativeAnchorPositionLayout.Invalidate(); } protected override void ClearNestedHitObjects() @@ -265,14 +267,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Size = SliderBody?.Size ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero; - if (!drawSizeLayout.IsValid) + if (!relativeAnchorPositionLayout.IsValid) { Vector2 pos = Vector2.Divide(OriginPosition, DrawSize); foreach (var obj in NestedHitObjects) obj.RelativeAnchorPosition = pos; Ball.RelativeAnchorPosition = pos; - drawSizeLayout.Validate(); + relativeAnchorPositionLayout.Validate(); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 97980c6d18..4d8381cf42 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,9 +1,11 @@ // 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -14,6 +16,22 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + protected override HitEvent CreateHitEvent(JudgementResult result) => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs index bb61bd37c1..9a5abba4fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs @@ -65,14 +65,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { this.RotateTo(-45); - this.ScaleTo(1.8f); + this.ScaleTo(1.6f); this.ScaleTo(1.2f, 100, Easing.In); - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + this.FadeOutFromOne(400); } else if (Result.IsMiss()) { + this.FadeOutFromOne(800); + this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); @@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon } else { + this.FadeOutFromOne(800); + JudgementText .FadeInFromZero(300, Easing.OutQuint) .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); } - this.FadeOutFromOne(800); - ringExplosion?.PlayAnimation(); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs index b65f46c414..272f4b5658 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs @@ -3,47 +3,39 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public partial class DefaultApproachCircle : SkinnableSprite + public partial class DefaultApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public DefaultApproachCircle() - : base("Gameplay/osu/approachcircle") - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - accentColour.BindTo(drawableObject.AccentColour); + Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); - accentColour.BindValueChanged(colour => Colour = colour.NewValue, true); - } - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // Although this is a non-legacy component, osu-resources currently stores approach circle as a legacy-like texture. - // See LegacyApproachCircle for documentation as to why this is required. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; + accentColour = drawableObject.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index f1143cf14d..512ac8ee3e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.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.Graphics; using osu.Game.Graphics.Backgrounds; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; + ClampAxes = Axes.None; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index eea6606233..0bdea0cab1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -1,9 +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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; @@ -12,40 +13,31 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - // todo: this should probably not be a SkinnableSprite, as this is always created for legacy skins and is recreated on skin change. - public partial class LegacyApproachCircle : SkinnableSprite + public partial class LegacyApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { - accentColour.BindTo(drawableObject.AccentColour); + var texture = skin.GetTexture(@"approachcircle"); + Debug.Assert(texture != null); + Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); + + accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } - - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // account for the sprite being used for the default approach circle being taken from stable, - // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index 780084115d..ad1fb98aef 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -34,19 +34,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject, ISkinSource skinSource) { + const string lookup_name = @"reversearrow"; + drawableRepeat = (DrawableSliderRepeat)drawableObject; AutoSizeAxes = Axes.Both; - string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; - - var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); + var skin = skinSource.FindProvider(s => s.GetTexture(lookup_name) != null); InternalChild = arrow = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), + Texture = skin?.GetTexture(lookup_name)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), }; textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index c01d28c8e1..d2ebc68c52 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -163,7 +163,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.ApproachCircle: - return new LegacyApproachCircle(); + if (GetTexture(@"approachcircle") != null) + return new LegacyApproachCircle(); + + return null; default: throw new UnsupportedSkinComponentException(lookup); diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index c89e2b727b..d1a8a048ed 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -1,17 +1,19 @@ // 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.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Taiko.Tests.Skinning { @@ -37,11 +39,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Beatmap.Value.Track.Start(); }); - AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield + AddStep("Load playfield", () => SetContents(_ => new Container { - Height = 0.2f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(2f, 1f), + Scale = new Vector2(0.5f), + Child = new TaikoPlayfieldAdjustmentContainer { Child = new TaikoPlayfield() }, })); } @@ -54,7 +59,20 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestHeightChanges() { - AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); + int value = 0; + + AddRepeatStep("change height", () => + { + value = (value + 1) % 5; + + this.ChildrenOfType().ForEach(p => + { + var parent = (Container)p.Parent.AsNonNull(); + parent.Scale = new Vector2(0.5f + 0.1f * value); + parent.Width = 1f / parent.Scale.X; + parent.Height = 0.5f / parent.Scale.Y; + }); + }, 50); } [Test] diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 48465bb119..4eb6b0ab3d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 2fd9f070ec..7e40d575bc 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { @@ -33,6 +35,22 @@ namespace osu.Game.Rulesets.Taiko.Scoring * strongScaleValue(result); } + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + public override int GetBaseScoreForResult(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs index 07a7f237ba..838f172186 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -143,16 +142,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (target != null) { - const float alpha_amount = 1; - const float down_time = 80; const float up_time = 50; - target.Animate( - t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time) - ).Then( - t => t.FadeOut(up_time) - ); + target + .FadeTo(1, down_time * (1 - target.Alpha), Easing.Out) + .Delay(100).FadeOut(up_time); } return false; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs index cf9d8dd52e..0b43f1c845 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs @@ -17,30 +17,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { RelativeSizeAxes = Axes.Both; - InternalChild = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + new Sprite { - new Sprite - { - Texture = skin.GetTexture("approachcircle"), - Scale = new Vector2(0.83f), - Alpha = 0.47f, // eyeballed to match stable - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Sprite - { - Texture = skin.GetTexture("taikobigcircle"), - Scale = new Vector2(0.8f), - Alpha = 0.22f, // eyeballed to match stable - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } + Texture = skin.GetTexture("approachcircle"), + Scale = new Vector2(0.83f), + Alpha = 0.47f, // eyeballed to match stable + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Sprite + { + Texture = skin.GetTexture("taikobigcircle"), + Scale = new Vector2(0.8f), + Alpha = 0.22f, // eyeballed to match stable + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, }; } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 6e7c8c3631..7e3967dc95 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -10,7 +10,6 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; @@ -23,6 +22,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -59,14 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); - Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic)); Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL")); Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL")); - Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001)); Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Is.Not.Empty); @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void AccuracyAndRankOfStableScorePreserved() + public void AccuracyOfStableScoreRecomputed() { var memoryStream = new MemoryStream(); @@ -261,15 +261,16 @@ namespace osu.Game.Tests.Beatmaps.Formats // and we want to emulate a stable score here using (var sw = new SerializationWriter(memoryStream, true)) { - sw.Write((byte)0); // ruleset id (osu!) + sw.Write((byte)3); // ruleset id (mania). + // mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test sw.Write("username"); // irrelevant to this test sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test - sw.Write((ushort)198); // count300 - sw.Write((ushort)1); // count100 + sw.Write((ushort)1); // count300 + sw.Write((ushort)0); // count100 sw.Write((ushort)0); // count50 - sw.Write((ushort)0); // countGeki + sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania) sw.Write((ushort)0); // countKatu sw.Write((ushort)1); // countMiss sw.Write(12345678); // total score, irrelevant to this test @@ -287,13 +288,54 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { - Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300))); - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); }); } [Test] - public void AccuracyAndRankOfLazerScorePreserved() + public void RankOfStableScoreUsesLazerDefinitions() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)195); // count300 + sw.Write((ushort)1); // count100 + sw.Write((ushort)4); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)0); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + }); + } + + [Test] + public void AccuracyRankAndTotalScoreOfLazerScorePreserved() { var ruleset = new OsuRuleset().RulesetInfo; @@ -321,8 +363,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537)); + Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null); Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); - Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); } @@ -415,6 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Ruleset = new OsuRuleset().RulesetInfo, Difficulty = new BeatmapDifficulty(), BeatmapVersion = beatmapVersion, + }, + // needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die + // when trying to recompute total score. + HitObjects = + { + new HitCircle() } }); } diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 8b066f860f..e960995c45 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; @@ -210,31 +211,6 @@ namespace osu.Game.Tests.Database AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); } - [Test] - public void TestNonLegacyScoreNotSubjectToUpgrades() - { - ScoreInfo scoreInfo = null!; - TestBackgroundDataStoreProcessor processor = null!; - - AddStep("Add score which requires upgrade (and has beatmap)", () => - { - Realm.Write(r => - { - r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) - { - TotalScoreVersion = 30000005, - LegacyTotalScore = 123456, - }); - }); - }); - - AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); - AddUntilStep("Wait for completion", () => processor.Completed); - - AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); - AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005)); - } - public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index ff954211eb..fbe5a0e4d7 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -56,24 +56,6 @@ namespace osu.Game.Tests.NonVisual.Skinning "Gameplay/osu/followpoint", 1 }, new object[] - { - new[] { "followpoint@2x", "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint@2x" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint", 1 - }, - new object[] { // Looking up a filename with extension specified should work. new[] { "followpoint.png" }, diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs index 378dd99664..dd4c372193 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -29,7 +29,8 @@ namespace osu.Game.Tests.Visual.Background ColourDark = Color4.Gray, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(0.9f) + Size = new Vector2(0.9f), + ClampAxes = Axes.None } }; } @@ -40,7 +41,10 @@ namespace osu.Game.Tests.Visual.Background AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s)); - AddToggleStep("Masking", m => triangles.Masking = m); + AddStep("ClampAxes X", () => triangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => triangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => triangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => triangles.ClampAxes = Axes.None); } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs index 01a2464b8e..4713852c0b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs @@ -86,7 +86,8 @@ namespace osu.Game.Tests.Visual.Background { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + ClampAxes = Axes.None } } }, @@ -128,7 +129,10 @@ namespace osu.Game.Tests.Visual.Background AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White); AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); - AddToggleStep("Masking", m => maskedTriangles.Masking = m); + AddStep("ClampAxes X", () => maskedTriangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => maskedTriangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => maskedTriangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => maskedTriangles.ClampAxes = Axes.None); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index f2b3351533..6c36e6729e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -210,6 +210,13 @@ namespace osu.Game.Tests.Visual.Editing switchPresets(-1); assertPreset(BeatDivisorType.Custom, 15); assertBeatSnap(15); + + setDivisorViaInput(24); + assertPreset(BeatDivisorType.Custom, 24); + switchPresets(1); + assertPreset(BeatDivisorType.Common); + switchPresets(-2); + assertPreset(BeatDivisorType.Triplets); } private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 534348bed3..98a97e1d23 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -129,10 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay checkRate(1); } - private const int max_frames_catchup = 50; - private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => - mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) { MaxCatchUpFrames = max_frames_catchup } + mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) .WithChild(consumer = new ClockConsumingChild())); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ec3b3e0822..73aa3be73d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -93,15 +93,12 @@ namespace osu.Game.Tests.Visual.Gameplay double currentTime = masterClock.CurrentTime; - bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime); + bool goingForward = currentTime >= lastStopTime; alwaysGoingForward &= goingForward; if (!goingForward) Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); - - if (masterClock.LastStopTime != null) - lastStopTime = masterClock.LastStopTime.Value; }; }); @@ -125,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); - AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime)); + AddAssert("continued playing forward", () => Player.LastResumeTime, () => Is.GreaterThanOrEqualTo(Player.LastPauseTime)); AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index e2ce3a014c..5e22e47572 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -383,6 +384,11 @@ namespace osu.Game.Tests.Visual.Gameplay AllowImportCompletion = new SemaphoreSlim(1); } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = false, + }; + protected override async Task ImportScore(Score score) { ScoreImportStarted = true; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index e975a85401..99f0ffb9d0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = frameStabilityContainer = new FrameStabilityContainer { - MaxCatchUpFrames = 1 + Child = new FakeLoad() } } }); @@ -56,6 +57,15 @@ namespace osu.Game.Tests.Visual.Gameplay Dependencies.CacheAs(frameStabilityContainer); } + private partial class FakeLoad : Drawable + { + protected override void Update() + { + base.Update(); + Thread.Sleep(1); + } + } + [SetUpSteps] public void SetupSteps() { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs index 1ecd38e1d3..83430b5665 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("create IPC sender channels", () => { - ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true }); + ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPort = OsuGame.IPC_PORT }); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 435dd77120..41a5603060 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -45,6 +46,16 @@ namespace osu.Game.Tests.Visual.Ranking addCircleStep(createScore(1, new OsuRuleset())); } + [Test] + public void TestOsuRankHidden() + { + addCircleStep(createScore(0, new OsuRuleset(), 20, true)); + addCircleStep(createScore(0.8, new OsuRuleset(), 5, true)); + addCircleStep(createScore(0.95, new OsuRuleset(), 0, true)); + addCircleStep(createScore(0.97, new OsuRuleset(), 1, true)); + addCircleStep(createScore(1, new OsuRuleset(), 0, true)); + } + [Test] public void TestCatchRank() { @@ -65,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking addCircleStep(createScore(1, new CatchRuleset())); } - private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () => + private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy}, {score.Statistics.GetValueOrDefault(HitResult.Miss)} miss)", () => { Children = new Drawable[] { @@ -92,10 +103,22 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private ScoreInfo createScore(double accuracy, Ruleset ruleset) + private ScoreInfo createScore(double accuracy, Ruleset ruleset, int missCount = 0, bool hidden = false) { var scoreProcessor = ruleset.CreateScoreProcessor(); + var statistics = new Dictionary + { + { HitResult.Miss, missCount }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + }; + + var mods = hidden + ? new[] { new OsuModHidden() } + : new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }; + return new ScoreInfo { User = new APIUser @@ -105,19 +128,13 @@ namespace osu.Game.Tests.Visual.Ranking }, BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Ruleset = ruleset.RulesetInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + Mods = mods, TotalScore = 2845370, Accuracy = accuracy, MaxCombo = 999, - Rank = scoreProcessor.RankFromAccuracy(accuracy), + Rank = scoreProcessor.RankFromScore(accuracy, statistics), Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } + Statistics = statistics, }; } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ab2e867255..866e20d063 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -71,15 +72,16 @@ namespace osu.Game.Tests.Visual.Ranking private int onlineScoreID = 1; - [TestCase(1, ScoreRank.X)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.2, ScoreRank.D)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + [TestCase(1, ScoreRank.X, 0)] + [TestCase(0.9999, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.A, 1)] + [TestCase(0.925, ScoreRank.A, 5)] + [TestCase(0.85, ScoreRank.B, 9)] + [TestCase(0.75, ScoreRank.C, 11)] + [TestCase(0.5, ScoreRank.D, 21)] + [TestCase(0.2, ScoreRank.D, 51)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount) { TestResultsScreen screen = null; @@ -91,6 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); score.Accuracy = accuracy; score.Rank = rank; + score.Statistics[HitResult.Miss] = missCount; return screen = createResultsScreen(score); }); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index ef6c16f2c4..7b08524240 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 5f642b14f5..e09d1be22c 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 2cc07dd9ed..3b00f103c4 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 587e6bbeed..d0ffbdd459 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -27,11 +27,6 @@ namespace osu.Game.Beatmaps { private readonly bool applyOffsets; - /// - /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. - /// - public readonly BindableDouble ExternalPauseFrequencyAdjust = new BindableDouble(1); - private readonly OffsetCorrectionClock? userGlobalOffsetClock; private readonly OffsetCorrectionClock? platformOffsetClock; private readonly OffsetCorrectionClock? userBeatmapOffsetClock; @@ -69,13 +64,13 @@ namespace osu.Game.Beatmaps { // Audio timings in general with newer BASS versions don't match stable. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock); // User per-beatmap offset will be applied to this final clock. - finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, ExternalPauseFrequencyAdjust); + finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock); } else { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c05831d043..6b2cb4ee74 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -142,6 +142,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.ReplaySettingsOverlay, true); + SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -421,6 +422,7 @@ namespace osu.Game.Configuration ProfileCoverExpanded, EditorLimitedDistanceSnap, ReplaySettingsOverlay, + ReplayPlaybackControlsExpanded, AutomaticallyDownloadMissingBeatmaps, EditorShowSpeedChanges, TouchDisableGameplayTaps, diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs similarity index 87% rename from osu.Game/BackgroundDataStoreProcessor.cs rename to osu.Game/Database/BackgroundDataStoreProcessor.cs index fc7db13d41..be0c83bdb3 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; @@ -22,7 +21,7 @@ using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; -namespace osu.Game +namespace osu.Game.Database { /// /// Performs background updating of data stores at startup. @@ -74,6 +73,7 @@ namespace osu.Game processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); + upgradeScoreRanks(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -355,7 +355,7 @@ namespace osu.Game realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; - StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager); + StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo)); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); @@ -376,6 +376,66 @@ namespace osu.Game completeNotification(notification, processedCount, scoreIds.Count, failedCount); } + private void upgradeScoreRanks() + { + Logger.Log("Querying for scores that need rank upgrades..."); + + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() + // must be done after materialisation, as realm doesn't support + // filtering on nested property predicates or projection via `.Select()` + .Where(s => s.Ruleset.IsLegacyRuleset()) + .Select(s => s.ID))); + + Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); + + if (scoreIds.Count == 0) + return; + + var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in scoreIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, scoreIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id)!; + s.Rank = StandardisedScoreMigrationTools.ComputeRank(s); + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; + }); + + ++processedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update rank score {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; + } + } + + completeNotification(notification, processedCount, scoreIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 8c73806cb5..403e73ab77 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -232,42 +232,60 @@ namespace osu.Game.Database } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompite the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// /// The score to update. - /// A used for lookups. - public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps) + /// The applicable for this score. + public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap) { - score.TotalScore = convertFromLegacyTotalScore(score, beatmaps); - score.Accuracy = ComputeAccuracy(score); + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + // warning: ordering is important here - both total score and ranks are dependent on accuracy! + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompute the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// + /// + /// This overload is intended for server-side flows. + /// See: https://github.com/ppy/osu-queue-score-statistics/blob/3681e92ac91c6c61922094bdbc7e92e6217dd0fc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs + /// /// The score to update. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. - public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + public static void UpdateFromLegacy(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { - score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes); - score.Accuracy = ComputeAccuracy(score); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + // warning: ordering is important here - both total score and ranks are dependent on accuracy! + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. - /// A used for lookups. + /// The in which the score was set. + /// The applicable for this score. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { if (!score.IsLegacyScore) return score.TotalScore; - WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); - Ruleset ruleset = score.Ruleset.CreateInstance(); - if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; @@ -283,24 +301,24 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); - return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return score.TotalScore; Debug.Assert(score.LegacyTotalScore != null); - Ruleset ruleset = score.Ruleset.CreateInstance(); if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; @@ -474,14 +492,9 @@ namespace osu.Game.Database break; case 3: - // in the mania case accuracy actually changes between score V1 and score V2 / standardised - // (PERFECT weighting changes from 300 to 305), - // so for better accuracy recompute accuracy locally based on hit statistics and use that instead, - double scoreV2Accuracy = ComputeAccuracy(score); - convertedTotalScore = (long)Math.Round(( 850000 * comboProportion - + 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy) + + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + bonusProportion) * modMultiplier); break; @@ -584,11 +597,8 @@ namespace osu.Game.Database } } - public static double ComputeAccuracy(ScoreInfo scoreInfo) + private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) { - Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); - int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) @@ -597,6 +607,18 @@ namespace osu.Game.Database return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + + private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + { + var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + + foreach (var mod in scoreInfo.Mods.OfType()) + rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + + return rank; + } + /// /// Used to populate the model using data parsed from its corresponding replay file. /// diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 1a78c1ec5e..e877915fac 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Lists; using osu.Framework.Bindables; @@ -78,10 +77,10 @@ namespace osu.Game.Graphics.Backgrounds } /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool Masking { get; set; } + public Axes ClampAxes { get; set; } = Axes.Both; /// /// Whether we should drop-off alpha values of triangles more quickly to improve @@ -258,13 +257,12 @@ namespace osu.Game.Graphics.Backgrounds private IShader shader; private Texture texture; - private bool masking; + private Axes clampAxes; private readonly List parts = new List(); private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; private Vector2 size; - private IVertexBatch vertexBatch; public TrianglesDrawNode(Triangles source) : base(source) @@ -278,7 +276,7 @@ namespace osu.Game.Graphics.Backgrounds shader = Source.shader; texture = Source.texture; size = Source.DrawSize; - masking = Source.Masking; + clampAxes = Source.ClampAxes; parts.Clear(); parts.AddRange(Source.parts); @@ -290,12 +288,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Draw(renderer); - if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -314,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -333,30 +325,35 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index a20fd78569..706b05f5ad 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -34,10 +33,10 @@ namespace osu.Game.Graphics.Backgrounds protected virtual bool CreateNewTriangles => true; /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool Masking { get; set; } + public Axes ClampAxes { get; set; } = Axes.Both; private readonly BindableFloat spawnRatio = new BindableFloat(1f); @@ -194,9 +193,7 @@ namespace osu.Game.Graphics.Backgrounds private Vector2 size; private float thickness; private float texelSize; - private bool masking; - - private IVertexBatch? vertexBatch; + private Axes clampAxes; public TrianglesDrawNode(TrianglesV2 source) : base(source) @@ -211,7 +208,7 @@ namespace osu.Game.Graphics.Backgrounds texture = Source.texture; size = Source.DrawSize; thickness = Source.Thickness; - masking = Source.Masking; + clampAxes = Source.ClampAxes; Quad triangleQuad = new Quad( Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix), @@ -235,12 +232,6 @@ namespace osu.Game.Graphics.Backgrounds if (Source.AimCount == 0 || thickness == 0) return; - if (vertexBatch == null || vertexBatch.Size != Source.AimCount) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -257,7 +248,7 @@ namespace osu.Game.Graphics.Backgrounds { Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -273,30 +264,35 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 5abc66d2ac..2ae4dc5a76 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.Containers protected override void PopIn() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Show(); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); @@ -132,7 +132,7 @@ namespace osu.Game.Graphics.Containers protected override void PopOut() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Hide(); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 26e499ae9a..a085558b3a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.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.IO; using System.Threading; @@ -24,6 +22,8 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace osu.Game.Graphics { @@ -37,30 +37,26 @@ namespace osu.Game.Graphics /// public IBindable CursorVisibility => cursorVisibility; - private Bindable screenshotFormat; - private Bindable captureMenuCursor; + [Resolved] + private GameHost host { get; set; } = null!; [Resolved] - private GameHost host { get; set; } + private Clipboard clipboard { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } - - private Storage storage; + private INotificationOverlay notificationOverlay { get; set; } = null!; [Resolved] - private INotificationOverlay notificationOverlay { get; set; } + private OsuConfigManager config { get; set; } = null!; - private Sample shutter; + private Storage storage = null!; + + private Sample? shutter; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, Storage storage, AudioManager audio) + private void load(Storage storage, AudioManager audio) { this.storage = storage.GetStorageForDirectory(@"screenshots"); - - screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); - captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); - shutter = audio.Samples.Get("UI/shutter"); } @@ -72,7 +68,7 @@ namespace osu.Game.Graphics switch (e.Action) { case GlobalAction.TakeScreenshot: - shutter.Play(); + shutter?.Play(); TakeScreenshotAsync().FireAndForget(); return true; } @@ -90,9 +86,12 @@ namespace osu.Game.Graphics { Interlocked.Increment(ref screenShotTasks); + ScreenshotFormat screenshotFormat = config.Get(OsuSetting.ScreenshotFormat); + bool captureMenuCursor = config.Get(OsuSetting.ScreenshotCaptureMenuCursor); + try { - if (!captureMenuCursor.Value) + if (!captureMenuCursor) { cursorVisibility.Value = false; @@ -101,7 +100,7 @@ namespace osu.Game.Graphics int framesWaited = 0; - using (var framesWaitedEvent = new ManualResetEventSlim(false)) + using (ManualResetEventSlim framesWaitedEvent = new ManualResetEventSlim(false)) { ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() => { @@ -117,17 +116,41 @@ namespace osu.Game.Graphics } } - using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) + using (Image? image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { + if (config.Get(OsuSetting.Scaling) == ScalingMode.Everything) + { + float posX = config.Get(OsuSetting.ScalingPositionX); + float posY = config.Get(OsuSetting.ScalingPositionY); + float sizeX = config.Get(OsuSetting.ScalingSizeX); + float sizeY = config.Get(OsuSetting.ScalingSizeY); + + image.Mutate(m => + { + Rectangle rect = new Rectangle(Point.Empty, m.GetCurrentSize()); + + // Reduce size by user scale settings... + int sx = (rect.Width - (int)(rect.Width * sizeX)) / 2; + int sy = (rect.Height - (int)(rect.Height * sizeY)) / 2; + rect.Inflate(-sx, -sy); + + // ...then adjust the region based on their positional offset. + rect.X = (int)(rect.X * posX) * 2; + rect.Y = (int)(rect.Y * posY) * 2; + + m.Crop(rect); + }); + } + clipboard.SetImage(image); - (string filename, var stream) = getWritableStream(); + (string? filename, Stream? stream) = getWritableStream(screenshotFormat); if (filename == null) return; using (stream) { - switch (screenshotFormat.Value) + switch (screenshotFormat) { case ScreenshotFormat.Png: await image.SaveAsPngAsync(stream).ConfigureAwait(false); @@ -140,7 +163,7 @@ namespace osu.Game.Graphics break; default: - throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}."); + throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat}."); } } @@ -164,12 +187,12 @@ namespace osu.Game.Graphics private static readonly object filename_reservation_lock = new object(); - private (string filename, Stream stream) getWritableStream() + private (string? filename, Stream? stream) getWritableStream(ScreenshotFormat format) { lock (filename_reservation_lock) { - var dt = DateTime.Now; - string fileExt = screenshotFormat.ToString().ToLowerInvariant(); + DateTime dt = DateTime.Now; + string fileExt = format.ToString().ToLowerInvariant(); string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}"; if (!storage.Exists(withoutIndex)) diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index fc0770d896..af4b3849af 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -33,7 +33,7 @@ namespace osu.Game.Graphics.UserInterface Current.ValueChanged += index => { - foreach (var t in TabContainer.Children.OfType()) + foreach (var t in TabContainer.OfType()) { int tIndex = TabContainer.IndexOf(t); int tabIndex = TabContainer.IndexOf(TabMap[index.NewValue]); diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c920597a95..c39f41bf72 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -150,6 +150,7 @@ namespace osu.Game.Graphics.UserInterface TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), Shear = new Vector2(-0.2f, 0), + ClampAxes = Axes.Y }, }, }, diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 05309760e7..c260c92b43 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface if (Dropdown is IHasAccentColour dropdown) dropdown.AccentColour = value; - foreach (var i in TabContainer.Children.OfType()) + foreach (var i in TabContainer.OfType()) i.AccentColour = value; } } @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - protected virtual float StripWidth => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; + protected virtual float StripWidth => TabContainer.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; /// /// Whether entries should be automatically populated if is an type. diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 255b2149f0..62cdefda43 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -4,7 +4,6 @@ #nullable disable using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; @@ -39,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface protected override double GetProportionalDuration(long currentValue, long newValue) => currentValue > newValue ? currentValue - newValue : newValue - currentValue; - protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(formatString); + protected override LocalisableString FormatCount(long count) => count.ToString(formatString); protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index fe986b275e..720f479216 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -101,7 +101,7 @@ namespace osu.Game.Graphics.UserInterface public void StopAnimation() { animate(current); - foreach (var star in stars.Children) + foreach (var star in stars) star.FinishTransforms(true); } diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 14a3c5a43c..d03d259f71 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -6,8 +6,8 @@ using System; using System.IO; using System.Linq; -using System.Threading; using osu.Framework.Platform; +using osu.Game.Utils; namespace osu.Game.IO { @@ -81,7 +81,7 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => fi.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -92,11 +92,11 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(target.Delete, throwOnFailure: false); return allFilesDeleted; } @@ -115,7 +115,7 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => fileInfo.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - AttemptOperation(() => + FileUtils.AttemptOperation(() => { fileInfo.Refresh(); @@ -139,35 +139,5 @@ namespace osu.Game.IO CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); } } - - /// - /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. - /// - /// The action to perform. - /// The number of attempts (250ms wait between each). - /// Whether to throw an exception on failure. If false, will silently fail. - protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) - { - while (true) - { - try - { - action(); - return true; - } - catch (Exception) - { - if (attempts-- == 0) - { - if (throwOnFailure) - throw; - - return false; - } - } - - Thread.Sleep(250); - } - } } } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5a39c02185..436334cfe1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), + new KeyBinding(InputKey.Comma, GlobalAction.StepReplayBackward), + new KeyBinding(InputKey.Period, GlobalAction.StepReplayForward), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; @@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings IncreaseOffset, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))] - DecreaseOffset + DecreaseOffset, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayForward))] + StepReplayForward, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] + StepReplayBackward, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs index 632a1ad0ea..b905b7ae1c 100644 --- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl"; /// - /// "Beatmap offset" + /// "Audio offset (this beatmap)" /// - public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset"); + public static LocalisableString AudioOffsetThisBeatmap => new TranslatableString(getKey(@"beatmap_offset"), @"Audio offset (this beatmap)"); /// /// "Previous play:" diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index ca27d0ff95..703e0ff1ca 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -324,6 +324,16 @@ namespace osu.Game.Localisation /// public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward"); + /// + /// "Seek replay forward one frame" + /// + public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame"); + + /// + /// "Step replay backward one frame" + /// + public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame"); + /// /// "Toggle chat focus" /// diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 1aedd9fc5b..60874da561 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -9,6 +9,16 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings"; + /// + /// "Step backward one frame" + /// + public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame"); + + /// + /// "Step forward one frame" + /// + public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 67f2590ad8..0fd9597ac0 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -287,7 +287,7 @@ namespace osu.Game.Online.Leaderboards double delay = 0; - foreach (var s in scoreFlowContainer.Children) + foreach (var s in scoreFlowContainer) { using (s.BeginDelayedSequence(delay)) s.Show(); @@ -384,7 +384,7 @@ namespace osu.Game.Online.Leaderboards if (scoreFlowContainer == null) return; - foreach (var c in scoreFlowContainer.Children) + foreach (var c in scoreFlowContainer) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y; float bottomY = topY + LeaderboardScore.HEIGHT; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0d6eb1ea44..c244708385 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -80,6 +80,13 @@ namespace osu.Game [Cached(typeof(OsuGame))] public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { +#if DEBUG + // Different port allows runnning release and debug builds alongside each other. + public const int IPC_PORT = 44824; +#else + public const int IPC_PORT = 44823; +#endif + /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). /// diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs index 809ea2f11d..1699dcceb0 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Chat.Listing flow.ChildrenEnumerable = newChannels.Where(c => c.Type == ChannelType.Public) .Select(c => new ChannelListingItem(c)); - foreach (var item in flow.Children) + foreach (var item in flow) { item.OnRequestJoin += channel => OnRequestJoin?.Invoke(channel); item.OnRequestLeave += channel => OnRequestLeave?.Invoke(channel); diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 1c56763bd9..b2c5a054e1 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Mods var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV(); var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f); - triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f)); + triangles.Colour = ColourInfo.GradientVertical(trianglesColour, value); } } @@ -95,6 +95,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Velocity = 0.7f, + ClampAxes = Axes.Y }, headerText = new OsuTextFlowContainer(t => { diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 7ec52364d4..ab99370603 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -405,6 +405,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(150), FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, new Box { diff --git a/osu.Game/Overlays/OverlayStreamControl.cs b/osu.Game/Overlays/OverlayStreamControl.cs index 84de384fb5..bc37a57cab 100644 --- a/osu.Game/Overlays/OverlayStreamControl.cs +++ b/osu.Game/Overlays/OverlayStreamControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -49,7 +49,7 @@ namespace osu.Game.Overlays protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 90f5a59215..ef1691534f 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -12,12 +12,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +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.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Screens.Play.PlayerSettings; using osuTK; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -67,7 +69,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Direction = FillDirection.Vertical, Children = new Drawable[] { - new TimeSlider + new OffsetSliderBar { RelativeSizeAxes = Axes.X, Current = { BindTarget = Current }, @@ -157,6 +159,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms."; applySuggestion.Enabled.Value = SuggestedOffset.Value != null; } + + private partial class OffsetSliderBar : RoundedSliderBar + { + public override LocalisableString TooltipText => BeatmapOffsetControl.GetOffsetExplanatoryText(Current.Value); + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 3ff5556f4d..fe88413e6a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -15,6 +15,7 @@ using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; +using osu.Game.Utils; using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General @@ -111,7 +112,8 @@ namespace osu.Game.Overlays.Settings.Sections.General using (var outStream = storage.CreateFileSafely(archive_filename)) using (var zip = ZipArchive.Create()) { - foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) zip.AddEntry(f, logStorage.GetStream(f), true); + foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) + FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); zip.SaveTo(outStream); } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index ada651b60e..7330f138ce 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -45,13 +45,14 @@ namespace osu.Game.Rulesets.Judgements if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { this.RotateTo(-45); - this.ScaleTo(1.8f); + this.ScaleTo(1.6f); this.ScaleTo(1.2f, 100, Easing.In); - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + this.FadeOutFromOne(400); + return; } - else if (Result.IsMiss()) + + if (Result.IsMiss()) { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 3b45acc7bb..1b0176cae5 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -153,6 +153,9 @@ namespace osu.Game.Rulesets.Objects.Pooling protected override bool CheckChildrenLife() { + if (!IsPresent) + return false; + bool aliveChanged = base.CheckChildrenLife(); aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); return aliveChanged; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 80e751422e..a092829317 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -370,7 +370,7 @@ namespace osu.Game.Rulesets.Scoring if (rank.Value == ScoreRank.F) return; - rank.Value = RankFromAccuracy(Accuracy.Value); + rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); foreach (var mod in Mods.Value.OfType()) rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); } @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public virtual ScoreRank RankFromAccuracy(double accuracy) + public virtual ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 2af9916a6b..8c9cb262af 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -25,17 +25,15 @@ namespace osu.Game.Rulesets.UI public ReplayInputHandler? ReplayInputHandler { get; set; } /// - /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. + /// The number of CPU milliseconds to spend at most during seek catch-up. /// - public int MaxCatchUpFrames { get; set; } = 5; + private const double max_catchup_milliseconds = 10; /// /// Whether to enable frame-stable playback. /// internal bool FrameStablePlayback { get; set; } = true; - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - private readonly Bindable isCatchingUp = new Bindable(); private readonly Bindable waitingOnFrames = new Bindable(); @@ -61,6 +59,8 @@ namespace osu.Game.Rulesets.UI /// private readonly FramedClock framedClock; + private readonly Stopwatch stopwatch = new Stopwatch(); + /// /// The current direction of playback to be exposed to frame stable children. /// @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - int loops = MaxCatchUpFrames; + stopwatch.Restart(); do { @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.UI base.UpdateSubTree(); UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); - } while (state == PlaybackState.RequiresCatchUp && loops-- > 0); + } while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds); return true; } @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.UI // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; } - else if (IsPaused.Value) + else if (IsPaused.Value && !hasReplayAttached) { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b30fc7aee1..e51a95798b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -19,6 +20,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -38,7 +40,6 @@ namespace osu.Game.Scoring.Legacy }; WorkingBeatmap workingBeatmap; - byte[] compressedScoreInfo = null; using (SerializationReader sr = new SerializationReader(stream)) { @@ -107,6 +108,8 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.LegacyOnlineID = sr.ReadInt32(); + byte[] compressedScoreInfo = null; + if (version >= 30000001) compressedScoreInfo = sr.ReadByteArray(); @@ -130,10 +133,12 @@ namespace osu.Game.Scoring.Legacy } } - if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) - PopulateLegacyAccuracyAndRank(score.ScoreInfo); - else - populateLazerAccuracyAndRank(score.ScoreInfo); + PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. @@ -171,121 +176,65 @@ namespace osu.Game.Scoring.Legacy } /// - /// Populates the accuracy of a given from its contained statistics. + /// Populates the for a given . /// - /// - /// Legacy use only. - /// - /// The to populate. - public static void PopulateLegacyAccuracyAndRank(ScoreInfo score) + /// The score to populate the statistics of. + /// The corresponding . + internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) { - int countMiss = score.GetCountMiss() ?? 0; - int count50 = score.GetCount50() ?? 0; - int count100 = score.GetCount100() ?? 0; - int count300 = score.GetCount300() ?? 0; - int countGeki = score.GetCountGeki() ?? 0; - int countKatu = score.GetCountKatu() ?? 0; + Debug.Assert(score.BeatmapInfo != null); - switch (score.Ruleset.OnlineID) + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) + return; + + var ruleset = score.Ruleset.Detach(); + var rulesetInstance = ruleset.CreateInstance(); + var scoreProcessor = rulesetInstance.CreateScoreProcessor(); + + // Populate the maximum statistics. + HitResult maxBasicResult = rulesetInstance.GetHitResults() + .Select(h => h.result) + .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); + + foreach ((HitResult result, int count) in score.Statistics) { - case 0: + switch (result) { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1; + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; + break; - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; + break; - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + case HitResult.SmallBonus: + case HitResult.LargeBonus: + break; - case 1: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 2: - { - int totalHits = count50 + count100 + count300 + countMiss + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.98) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.94) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.85) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 3: - { - int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.95) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.8) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.7) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; + default: + score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; + break; } } - } - private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo) - { - scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); + if (!score.IsLegacyScore) + return; - var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy); +#pragma warning disable CS0618 + // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. + // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. + var calculator = rulesetInstance.CreateDifficultyCalculator(workingBeatmap); + var attributes = calculator.Calculate(score.Mods); - foreach (var mod in scoreInfo.Mods.OfType()) - rank = mod.AdjustRank(rank, scoreInfo.Accuracy); - - scoreInfo.Rank = rank; + int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); + if (attributes.MaxCombo > maxComboFromStatistics) + score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; +#pragma warning restore CS0618 } private void readLegacyReplay(Replay replay, StreamReader reader) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 389b20b5c8..c74980abb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -43,9 +43,10 @@ namespace osu.Game.Scoring.Legacy /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct /// method. Reconvert all scores. /// + /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// /// - public const int LATEST_VERSION = 30000012; + public const int LATEST_VERSION = 30000013; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 8e28707107..768c28cc38 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,7 +17,6 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using Realms; namespace osu.Game.Scoring @@ -91,8 +90,6 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); - PopulateMaximumStatistics(model); - if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); @@ -103,75 +100,6 @@ namespace osu.Game.Scoring // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); - else if (model.IsLegacyScore) - { - model.LegacyTotalScore = model.TotalScore; - StandardisedScoreMigrationTools.UpdateFromLegacy(model, beatmaps()); - } - } - - /// - /// Populates the for a given . - /// - /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) - { - Debug.Assert(score.BeatmapInfo != null); - - if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) - return; - - var beatmap = score.BeatmapInfo!.Detach(); - var ruleset = score.Ruleset.Detach(); - var rulesetInstance = ruleset.CreateInstance(); - var scoreProcessor = rulesetInstance.CreateScoreProcessor(); - - Debug.Assert(rulesetInstance != null); - - // Populate the maximum statistics. - HitResult maxBasicResult = rulesetInstance.GetHitResults() - .Select(h => h.result) - .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); - - foreach ((HitResult result, int count) in score.Statistics) - { - switch (result) - { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; - break; - - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; - break; - - case HitResult.IgnoreHit: - case HitResult.IgnoreMiss: - case HitResult.SmallBonus: - case HitResult.LargeBonus: - break; - - default: - score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; - break; - } - } - - if (!score.IsLegacyScore) - return; - -#pragma warning disable CS0618 - // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. - // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. - var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap)); - var attributes = calculator.Calculate(score.Mods); - - int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); - if (attributes.MaxCombo > maxComboFromStatistics) - score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; -#pragma warning restore CS0618 } // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 02d9e0a280..1ee99e9e93 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -26,6 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { + private readonly Func beatmaps; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; @@ -44,6 +46,7 @@ namespace osu.Game.Scoring OsuConfigManager configManager = null) : base(storage, realm) { + this.beatmaps = beatmaps; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -171,7 +174,11 @@ namespace osu.Game.Scoring /// Populates the for a given . /// /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public void PopulateMaximumStatistics(ScoreInfo score) + { + Debug.Assert(score.BeatmapInfo != null); + LegacyScoreDecoder.PopulateMaximumStatistics(score, beatmaps().GetWorkingBeatmap(score.BeatmapInfo.Detach())); + } #region Implementation of IPresentImports diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index ffa4f01e75..4b0726658f 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -29,10 +29,11 @@ namespace osu.Game.Screens.Edit /// Set a divisor, updating the valid divisor range appropriately. /// /// The intended divisor. - public void SetArbitraryDivisor(int divisor) + /// Forces changing the valid divisors to a known preset. + public void SetArbitraryDivisor(int divisor, bool preferKnownPresets = false) { // If the current valid divisor range doesn't contain the proposed value, attempt to find one which does. - if (!ValidDivisors.Value.Presets.Contains(divisor)) + if (preferKnownPresets || !ValidDivisors.Value.Presets.Contains(divisor)) { if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor)) ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index b33edb9edb..da1a37d57f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -208,11 +208,11 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (currentType) { case BeatDivisorType.Common: - beatDivisor.SetArbitraryDivisor(4); + beatDivisor.SetArbitraryDivisor(4, true); break; case BeatDivisorType.Triplets: - beatDivisor.SetArbitraryDivisor(6); + beatDivisor.SetArbitraryDivisor(6, true); break; case BeatDivisorType.Custom: diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 110beb0fa6..2d6e234e57 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { bool selectionPerformed = performMouseDownActions(e); - bool movementPossible = prepareSelectionMovement(); + bool movementPossible = prepareSelectionMovement(e); // check if selection has occurred if (selectionPerformed) @@ -536,9 +536,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Attempts to begin the movement of any selected blueprints. /// + /// The defining the beginning of a movement. /// Whether a movement is possible. - private bool prepareSelectionMovement() + private bool prepareSelectionMovement(MouseDownEvent e) { + if (e.Button == MouseButton.Right) + return false; + if (!SelectionHandler.SelectedBlueprints.Any()) return false; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index b2b3fbd626..d742d2377f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -343,7 +343,7 @@ namespace osu.Game.Screens.Menu { buttonArea.ButtonSystemState = state; - foreach (var b in buttonArea.Children.OfType()) + foreach (var b in buttonArea.OfType()) b.ButtonSystemState = state; } diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index fd59ec3573..dd5171c6be 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Skinning; @@ -43,8 +44,6 @@ namespace osu.Game.Screens.Menu private const double shoot_duration = 800; - protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; - [Resolved] private ISkinSource skin { get; set; } = null!; @@ -57,7 +56,6 @@ namespace osu.Game.Screens.Menu private void load(TextureStore textures) { Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); - Active.Value = true; } protected override FallingParticle CreateParticle() @@ -81,8 +79,15 @@ namespace osu.Game.Screens.Menu return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } + private ScheduledDelegate? deactivateDelegate; + public void Shoot(int direction) { + Active.Value = true; + + deactivateDelegate?.Cancel(); + deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, shoot_duration); + lastShootTime = Clock.CurrentTime; lastShootDirection = direction; } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6f5ff9c99c..3792a67896 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -40,8 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { public override string Title => "Lounge"; - protected override bool PlayExitSound => false; - protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { SelectedRoom = { BindTarget = SelectedRoom } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 8a85a46a2f..a37314de0e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -44,8 +44,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override string ShortTitle => "room"; - protected override bool PlayExitSound => !exitConfirmed; - [Resolved] private MultiplayerClient client { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index b527bf98a2..fa1ee004c9 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; + protected sealed override bool PlayExitSound => false; + [Resolved] protected IRoomManager? RoomManager { get; private set; } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 490a1ae6b8..f719ef67c9 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -223,7 +223,12 @@ namespace osu.Game.Screens public override bool OnExiting(ScreenExitEvent e) { - if (ValidForResume && PlayExitSound) + // Only play the exit sound if we are the last screen in the exit sequence. + // This stops many sample playbacks from stacking when a huge screen purge happens (ie. returning to menu via the home button + // from a deeply nested screen). + bool arrivingAtFinalDestination = e.Next == e.Destination; + + if (ValidForResume && PlayExitSound && arrivingAtFinalDestination) sampleExit?.Play(); if (ValidForResume && logo != null) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 4def1d36bb..c039d1e535 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -78,8 +78,6 @@ namespace osu.Game.Screens.Play isPaused.Value = false; - PrepareStart(); - // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), // this means that the first frame ever exposed to children may have a non-zero current time. @@ -99,14 +97,6 @@ namespace osu.Game.Screens.Play }); } - /// - /// When is called, this will be run to give an opportunity to prepare the clock at the correct - /// start location. - /// - protected virtual void PrepareStart() - { - } - /// /// Seek to a specific time in gameplay. /// diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index f16669f865..f8c82feddd 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -42,35 +42,30 @@ namespace osu.Game.Screens.Play.HUD Origin = anchor; AutoSizeAxes = Axes.Both; - InternalChild = new FillFlowContainer + InternalChildren = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + labelText = new OsuSpriteText { - labelText = new OsuSpriteText + Alpha = 0, + Text = label.GetValueOrDefault(), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), + Margin = new MarginPadding { Left = 2.5f }, + }, + NumberContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new[] { - Alpha = 0, - Text = label.GetValueOrDefault(), - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), - Margin = new MarginPadding { Left = 2.5f }, - }, - NumberContainer = new Container - { - AutoSizeAxes = Axes.Both, - Children = new[] + wireframesPart = new ArgonCounterSpriteText(wireframesLookup) { - wireframesPart = new ArgonCounterSpriteText(wireframesLookup) - { - Anchor = anchor, - Origin = anchor, - }, - textPart = new ArgonCounterSpriteText(textLookup) - { - Anchor = anchor, - Origin = anchor, - }, - } + Anchor = anchor, + Origin = anchor, + }, + textPart = new ArgonCounterSpriteText(textLookup) + { + Anchor = anchor, + Origin = anchor, + }, } } }; @@ -110,7 +105,11 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); - ShowLabel.BindValueChanged(s => labelText.Alpha = s.NewValue ? 1 : 0, true); + ShowLabel.BindValueChanged(s => + { + labelText.Alpha = s.NewValue ? 1 : 0; + NumberContainer.Y = s.NewValue ? 12 : 0; + }, true); } private partial class ArgonCounterSpriteText : OsuSpriteText diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 348327d710..44b9fb3123 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } - protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); + protected override LocalisableString FormatCount(long count) => count.ToString(); protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d990af32e7..d2b6b834f8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; // logic is mostly shared with Leaderboard, copied here for simplicity. - foreach (var c in Flow.Children) + foreach (var c in Flow) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y; float bottomY = topY + panel_height; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 326be55222..25e5464205 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter CounterFlow.Direction = convertedDirection; - foreach (var counter in CounterFlow.Children) + foreach (var counter in CounterFlow) counter.Direction.Value = convertedDirection; }, true); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 128f8d5ffd..32ebb82f15 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -99,6 +98,9 @@ namespace osu.Game.Screens.Play private readonly SkinComponentsContainer mainComponents; + [CanBeNull] + private readonly SkinComponentsContainer rulesetComponents; + /// /// A flow which sits at the left side of the screen to house leaderboard (and related) components. /// Will automatically be positioned to avoid colliding with top scoring elements. @@ -111,7 +113,6 @@ namespace osu.Game.Screens.Play public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { - Drawable rulesetComponents; this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -125,8 +126,8 @@ namespace osu.Game.Screens.Play clicksPerSecondController = new ClicksPerSecondController(), InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, - rulesetComponents = drawableRuleset != null - ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } + drawableRuleset != null + ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), playfieldComponents = drawableRuleset != null ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } @@ -170,7 +171,10 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, rulesetComponents, playfieldComponents, topRightElements }; + hideTargets = new List { mainComponents, playfieldComponents, topRightElements }; + + if (rulesetComponents != null) + hideTargets.Add(rulesetComponents); if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -254,39 +258,13 @@ namespace osu.Game.Screens.Play Vector2? highestBottomScreenSpace = null; - // LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. - foreach (var element in mainComponents.Components.Cast()) + foreach (var element in mainComponents.Components) + processDrawable(element); + + if (rulesetComponents != null) { - // for now align some top components with the bottom-edge of the lowest top-anchored hud element. - if (element.Anchor.HasFlagFast(Anchor.y0)) - { - // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. - if (element is LegacyHealthDisplay) - continue; - - float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y; - - bool isRelativeX = element.RelativeSizeAxes == Axes.X; - - if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX) - { - if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value) - lowestTopScreenSpaceRight = bottom; - } - - if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX) - { - if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value) - lowestTopScreenSpaceLeft = bottom; - } - } - // and align bottom-right components with the top-edge of the highest bottom-anchored hud element. - else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X)) - { - var topLeft = element.ScreenSpaceDrawQuad.TopLeft; - if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) - highestBottomScreenSpace = topLeft; - } + foreach (var element in rulesetComponents.Components) + processDrawable(element); } if (lowestTopScreenSpaceRight.HasValue) @@ -303,6 +281,43 @@ namespace osu.Game.Screens.Play bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; + + void processDrawable(ISerialisableDrawable element) + { + // Cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. + Drawable drawable = (Drawable)element; + + // for now align some top components with the bottom-edge of the lowest top-anchored hud element. + if (drawable.Anchor.HasFlagFast(Anchor.y0)) + { + // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. + if (element is LegacyHealthDisplay) + return; + + float bottom = drawable.ScreenSpaceDrawQuad.BottomRight.Y; + + bool isRelativeX = drawable.RelativeSizeAxes == Axes.X; + + if (drawable.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX) + { + if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value) + lowestTopScreenSpaceRight = bottom; + } + + if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX) + { + if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value) + lowestTopScreenSpaceLeft = bottom; + } + } + // and align bottom-right components with the top-edge of the highest bottom-anchored hud element. + else if (drawable.Anchor.HasFlagFast(Anchor.BottomRight) || (drawable.Anchor.HasFlagFast(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) + { + var topLeft = element.ScreenSpaceDrawQuad.TopLeft; + if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) + highestBottomScreenSpace = topLeft; + } + } } private void updateVisibility() diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 12f541167f..93bdcb1cab 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -60,17 +59,6 @@ namespace osu.Game.Screens.Play private readonly double skipTargetTime; - /// - /// Stores the time at which the last call was triggered. - /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp. - /// - /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled - /// to avoid fails occurring after the pause screen has been shown. - /// - /// In the future I want to change this. - /// - internal double? LastStopTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -113,71 +101,17 @@ namespace osu.Game.Screens.Play return time; } - protected override void StopGameplayClock() - { - LastStopTime = GameplayClock.CurrentTime; - - if (IsLoaded) - { - // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => - { - if (IsPaused.Value) - base.StopGameplayClock(); - }); - } - else - { - base.StopGameplayClock(); - - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 0; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } - } - public override void Seek(double time) { - // Safety in case the clock is seeked while stopped. - LastStopTime = null; elapsedValidationTime = null; base.Seek(time); } - protected override void PrepareStart() - { - if (LastStopTime != null) - { - Seek(LastStopTime.Value); - LastStopTime = null; - } - else - base.PrepareStart(); - } - protected override void StartGameplayClock() { addAdjustmentsToTrack(); - base.StartGameplayClock(); - - if (IsLoaded) - { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); - } - else - { - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 1; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } } /// @@ -273,7 +207,6 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); track.BindAdjustments(AdjustmentsFromMods); - track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.AddAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = true; @@ -285,7 +218,6 @@ namespace osu.Game.Screens.Play return; track.UnbindAdjustments(AdjustmentsFromMods); - track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.RemoveAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = false; diff --git a/osu.Game/Screens/Play/OffsetCorrectionClock.cs b/osu.Game/Screens/Play/OffsetCorrectionClock.cs index 207980f45c..e83ed7e464 100644 --- a/osu.Game/Screens/Play/OffsetCorrectionClock.cs +++ b/osu.Game/Screens/Play/OffsetCorrectionClock.cs @@ -1,15 +1,12 @@ // 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.Bindables; using osu.Framework.Timing; namespace osu.Game.Screens.Play { public class OffsetCorrectionClock : FramedOffsetClock { - private readonly BindableDouble pauseRateAdjust; - private double offset; public new double Offset @@ -28,10 +25,9 @@ namespace osu.Game.Screens.Play public double RateAdjustedOffset => base.Offset; - public OffsetCorrectionClock(IClock source, BindableDouble pauseRateAdjust) + public OffsetCorrectionClock(IClock source) : base(source) { - this.pauseRateAdjust = pauseRateAdjust; } public override void ProcessFrame() @@ -42,12 +38,8 @@ namespace osu.Game.Screens.Play private void updateOffset() { - // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. - if (pauseRateAdjust.Value == 1) - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - base.Offset = Offset * Rate; - } + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + base.Offset = Offset * Rate; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index db39d06c54..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1240,9 +1240,14 @@ namespace osu.Game.Screens.Play { b.IgnoreUserSettings.Value = true; - b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); - b.IsBreakTime.Value = false; + // May be null if the load never completed. + if (breakTracker != null) + { + b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); + b.IsBreakTime.Value = false; + } }); + storyboardReplacesBackground.Value = false; } } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 8efb80e771..9039604471 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new OffsetSliderBar { KeyboardStep = 5, - LabelText = BeatmapOffsetControlStrings.BeatmapOffset, + LabelText = BeatmapOffsetControlStrings.AudioOffsetThisBeatmap, Current = Current, }, referenceScoreContainer = new FillFlowContainer @@ -307,7 +307,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } } - public partial class OffsetSliderBar : PlayerSliderBar + private partial class OffsetSliderBar : PlayerSliderBar { protected override Drawable CreateControl() => new CustomSliderBar(); diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 44cfa8d811..b3d07421ed 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,19 +1,17 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Screens.Edit.Timing; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.Play.PlayerSettings { @@ -28,26 +26,28 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.01, }; - private readonly PlayerSliderBar rateSlider; + private PlayerSliderBar rateSlider = null!; - private readonly OsuSpriteText multiplierText; + private OsuSpriteText multiplierText = null!; - private readonly BindableBool isPaused = new BindableBool(); + private readonly IBindable isPaused = new BindableBool(); [Resolved] - private GameplayClockContainer? gameplayClock { get; set; } + private ReplayPlayer replayPlayer { get; set; } = null!; [Resolved] - private GameplayState? gameplayState { get; set; } + private GameplayClockContainer gameplayClock { get; set; } = null!; + + private IconButton pausePlay = null!; public PlaybackSettings() : base("playback") { - const double seek_amount = 5000; - const double seek_fast_amount = 10000; - - IconButton play; + } + [BackgroundDependencyLoader] + private void load() + { Children = new Drawable[] { new FillFlowContainer @@ -71,50 +71,62 @@ namespace osu.Game.Screens.Play.PlayerSettings Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, - Action = () => seek(-1, seek_fast_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000), + Action = () => replayPlayer.SeekInDirection(-10), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Backward, - Action = () => seek(-1, seek_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), + Action = () => replayPlayer.SeekInDirection(-1), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, - play = new IconButton + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepBackward, + Action = () => replayPlayer.StepFrame(-1), + TooltipText = PlayerSettingsOverlayStrings.StepBackward, + }, + pausePlay = new IconButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.4f), IconScale = new Vector2(1.4f), - Icon = FontAwesome.Regular.PlayCircle, Action = () => { - if (gameplayClock != null) - { - if (gameplayClock.IsRunning) - gameplayClock.Stop(); - else - gameplayClock.Start(); - } + if (gameplayClock.IsRunning) + gameplayClock.Stop(); + else + gameplayClock.Start(); }, }, new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepForward, + Action = () => replayPlayer.StepFrame(1), + TooltipText = PlayerSettingsOverlayStrings.StepForward, + }, + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, - Action = () => seek(1, seek_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000), + Action = () => replayPlayer.SeekInDirection(1), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastForward, - Action = () => seek(1, seek_fast_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000), + Action = () => replayPlayer.SeekInDirection(10), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, }, }, @@ -141,26 +153,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }, }; - - isPaused.BindValueChanged(paused => - { - if (!paused.NewValue) - { - play.TooltipText = ToastStrings.PauseTrack; - play.Icon = FontAwesome.Regular.PauseCircle; - } - else - { - play.TooltipText = ToastStrings.PlayTrack; - play.Icon = FontAwesome.Regular.PlayCircle; - } - }, true); - - void seek(int direction, double amount) - { - double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0); - gameplayClock?.Seek(target); - } } protected override void LoadComplete() @@ -168,8 +160,20 @@ namespace osu.Game.Screens.Play.PlayerSettings base.LoadComplete(); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true); - if (gameplayClock != null) - isPaused.BindTarget = gameplayClock.IsPaused; + isPaused.BindTo(gameplayClock.IsPaused); + isPaused.BindValueChanged(paused => + { + if (!paused.NewValue) + { + pausePlay.TooltipText = ToastStrings.PauseTrack; + pausePlay.Icon = FontAwesome.Regular.PauseCircle; + } + else + { + pausePlay.TooltipText = ToastStrings.PlayTrack; + pausePlay.Icon = FontAwesome.Regular.PlayCircle; + } + }, true); } private partial class SeekButton : IconButton diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 805f907466..3c5b85662a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -22,8 +23,11 @@ using osu.Game.Users; namespace osu.Game.Screens.Play { + [Cached] public partial class ReplayPlayer : Player, IKeyBindingHandler { + public const double BASE_SEEK_AMOUNT = 1000; + private readonly Func, Score> createScore; private readonly bool replayIsFailedScore; @@ -52,7 +56,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { if (!LoadedBeatmapSuccessfully) return; @@ -60,7 +64,7 @@ namespace osu.Game.Screens.Play var playbackSettings = new PlaybackSettings { Depth = float.MaxValue, - Expanded = { Value = false } + Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) } }; if (GameplayClockContainer is MasterGameplayClockContainer master) @@ -92,16 +96,22 @@ namespace osu.Game.Screens.Play public bool OnPressed(KeyBindingPressEvent e) { - const double keyboard_seek_amount = 5000; - switch (e.Action) { + case GlobalAction.StepReplayBackward: + StepFrame(-1); + return true; + + case GlobalAction.StepReplayForward: + StepFrame(1); + return true; + case GlobalAction.SeekReplayBackward: - keyboardSeek(-1); + SeekInDirection(-5); return true; case GlobalAction.SeekReplayForward: - keyboardSeek(1); + SeekInDirection(5); return true; case GlobalAction.TogglePauseReplay: @@ -113,13 +123,28 @@ namespace osu.Game.Screens.Play } return false; + } - void keyboardSeek(int direction) - { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime()); + public void StepFrame(int direction) + { + GameplayClockContainer.Stop(); - Seek(target); - } + var frames = GameplayState.Score.Replay.Frames; + + if (frames.Count == 0) + return; + + GameplayClockContainer.Seek(direction < 0 + ? (frames.LastOrDefault(f => f.Time < GameplayClockContainer.CurrentTime) ?? frames.First()).Time + : (frames.FirstOrDefault(f => f.Time > GameplayClockContainer.CurrentTime) ?? frames.Last()).Time + ); + } + + public void SeekInDirection(float amount) + { + double target = Math.Clamp(GameplayClockContainer.CurrentTime + amount * BASE_SEEK_AMOUNT, 0, GameplayState.Beatmap.GetLastObjectTime()); + + Seek(target); } public void OnReleased(KeyBindingReleaseEvent e) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 8cbca74466..0aff98df2b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -111,6 +112,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly double accuracyD; private readonly bool withFlair; + private readonly bool isFailedSDueToMisses; + private RankText failedSRankText; + public AccuracyCircle(ScoreInfo score, bool withFlair = false) { this.score = score; @@ -119,10 +123,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); + accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); + + isFailedSDueToMisses = score.Accuracy >= accuracyS && score.Rank == ScoreRank.A; } [BackgroundDependencyLoader] @@ -249,6 +256,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { + if (isFailedSDueToMisses) + AddInternal(failedSRankText = new RankText(ScoreRank.S)); + AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), @@ -387,6 +397,31 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); } } + + if (isFailedSDueToMisses) + { + const double adjust_duration = 200; + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY - adjust_duration)) + { + failedSRankText.FadeIn(adjust_duration); + + using (BeginDelayedSequence(adjust_duration)) + { + failedSRankText + .FadeColour(Color4.Red, 800, Easing.Out) + .RotateTo(10, 1000, Easing.Out) + .MoveToY(100, 1000, Easing.In) + .FadeOut(800, Easing.Out); + + accuracyCircle + .FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); + + badges.Single(b => b.Rank == getRank(ScoreRank.S)) + .FadeOut(70, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 7af327828e..8aea6045eb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// private readonly double displayPosition; - private readonly ScoreRank rank; + public readonly ScoreRank Rank; private Drawable rankContainer; private Drawable overlay; @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { Accuracy = accuracy; displayPosition = position; - this.rank = rank; + Rank = rank; RelativeSizeAxes = Axes.Both; Alpha = 0; @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Size = new Vector2(28, 14), Children = new[] { - new DrawableRank(rank), + new DrawableRank(Rank), overlay = new CircularContainer { RelativeSizeAxes = Axes.Both, @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Colour = OsuColour.ForRank(Rank).Opacity(0.2f), Radius = 10, }, Child = new Box diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4408634787..70ecde3858 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -868,7 +868,7 @@ namespace osu.Game.Screens.Select { var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - foreach (var panel in Scroll.Children) + foreach (var panel in Scroll) { Debug.Assert(panel.Item != null); @@ -899,7 +899,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in Scroll.Children) + foreach (DrawableCarouselItem item in Scroll) { updateItem(item); @@ -1094,7 +1094,7 @@ namespace osu.Game.Screens.Select // to enter clamp-special-case mode where it animates completely differently to normal. float scrollChange = scrollTarget.Value - Scroll.Current; Scroll.ScrollTo(scrollTarget.Value, false); - foreach (var i in Scroll.Children) + foreach (var i in Scroll) i.Y += scrollChange; break; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 369db37e63..bd659d7423 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -242,7 +242,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in beatmapContainer.Children) + foreach (var panel in beatmapContainer) { Debug.Assert(panel.Item != null); diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs index a44bf3a43d..ec159873f8 100644 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -28,8 +27,5 @@ namespace osu.Game.Skinning protected virtual string RulesetPrefix => string.Empty; protected virtual string ComponentName => Component.ToString(); - - public string LookupName => - string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 1834a17279..a9f68bd378 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -63,14 +63,10 @@ namespace osu.Game.Skinning // missed ticks / slider end don't get the normal animation. if (isMissedTick()) { - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); + this.ScaleTo(1.2f); + this.ScaleTo(1f, 100, Easing.In); - if (legacyVersion > 1.0m) - { - this.MoveTo(new Vector2(0, -2f)); - this.MoveToOffset(new Vector2(0, 10), fade_out_delay + fade_out_length, Easing.In); - } + this.FadeOutFromOne(400); } else { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 10913e7369..cfa5f972d2 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -491,39 +491,30 @@ namespace osu.Game.Skinning break; } - foreach (string name in getFallbackNames(componentName)) + Texture? texture = null; + float ratio = 1; + + if (AllowHighResolutionSprites) { - string lookupName = name; - Texture? texture = null; - float ratio = 1; + // some component names (especially user-controlled ones, like `HitX` in mania) + // may contain `@2x` scale specifications. + // stable happens to check for that and strip them, so do the same to match stable behaviour. + componentName = componentName.Replace(@"@2x", string.Empty); - if (AllowHighResolutionSprites) - { - // some component names (especially user-controlled ones, like `HitX` in mania) - // may contain `@2x` scale specifications. - // stable happens to check for that and strip them, so do the same to match stable behaviour. - lookupName = name.Replace(@"@2x", string.Empty); + string twoTimesFilename = $"{Path.ChangeExtension(componentName, null)}@2x{Path.GetExtension(componentName)}"; - string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); - texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); + if (texture != null) ratio = 2; - } - - if (texture == null) - { - ratio = 1; - texture = Textures?.Get(lookupName, wrapModeS, wrapModeT); - } - - if (texture == null) - continue; - - texture.ScaleAdjust = ratio; - return texture; } - return null; + texture ??= Textures?.Get(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust = ratio; + + return texture; } public override ISample? GetSample(ISampleInfo sampleInfo) @@ -534,7 +525,7 @@ namespace osu.Game.Skinning lookupNames = getLegacyLookupNames(hitSample); else { - lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackNames); + lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackSampleNames); } foreach (string lookup in lookupNames) @@ -552,7 +543,7 @@ namespace osu.Game.Skinning private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) { - var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames); + var lookupNames = hitSample.LookupNames.SelectMany(getFallbackSampleNames); if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) { @@ -571,13 +562,13 @@ namespace osu.Game.Skinning yield return hitSample.Name; } - private IEnumerable getFallbackNames(string componentName) + private IEnumerable getFallbackSampleNames(string name) { - // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. - yield return componentName; + // May be something like "Gameplay/normal-hitnormal" from lazer. + yield return name; - // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). - yield return componentName.Split('/').Last(); + // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/normal-hitnormal" -> "normal-hitnormal"). + yield return name.Split('/').Last(); } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index cefd51b2aa..fae9ec7f2e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -129,7 +129,7 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - var skinTextures = skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, null, out _); + var skinTextures = skin.GetTextures(Path.ChangeExtension(Animation.Path, null), default, default, true, string.Empty, null, out _); if (skinTextures.Length > 0) { diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index f3c69201e2..00e5b38b1a 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests [CallerMemberName] string callingMethodName = @"") : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions { - BindIPC = bindIPC, + IPCPort = bindIPC ? OsuGame.IPC_PORT : null, }, bypassCleanup: bypassCleanupOnDispose, realtime: realtime) { this.bypassCleanupOnSetup = bypassCleanupOnSetup; diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index e04c71d193..1a9e03b2a4 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Game/Utils/FileUtils.cs b/osu.Game/Utils/FileUtils.cs new file mode 100644 index 0000000000..063ab178f7 --- /dev/null +++ b/osu.Game/Utils/FileUtils.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading; + +namespace osu.Game.Utils +{ + public static class FileUtils + { + /// + /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. + /// + /// The action to perform. + /// The provided state. + /// The number of attempts (250ms wait between each). + /// Whether to throw an exception on failure. If false, will silently fail. + public static bool AttemptOperation(Action action, T state, int attempts = 10, bool throwOnFailure = true) + { + while (true) + { + try + { + action(state); + return true; + } + catch (Exception) + { + if (attempts-- == 0) + { + if (throwOnFailure) + throw; + + return false; + } + } + + Thread.Sleep(250); + } + } + + /// + /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. + /// + /// The action to perform. + /// The number of attempts (250ms wait between each). + /// Whether to throw an exception on failure. If false, will silently fail. + public static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) + { + while (true) + { + try + { + action(); + return true; + } + catch (Exception) + { + if (attempts-- == 0) + { + if (throwOnFailure) + throw; + + return false; + } + } + + Thread.Sleep(250); + } + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f33ddac66f..1b1abe3971 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,12 +21,12 @@ - + - - - - + + + + @@ -36,13 +36,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - - + + +