diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs index cae19e9468..180cb98ed7 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs index 57c06e1cd1..83db9f665b 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs index ce06b841aa..3afb8c3d89 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs index 9e38913be7..c537897439 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs index bec0a6a1d3..309393b664 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs index a302f95966..f4b9cf3b88 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs index 014954dd60..8d48e3acde 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 4cc712060c..748725af9f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModNightcore : ModNightcore { - public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index b2e4e07526..bb424eb587 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -21,7 +21,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI.Cursor; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } = null!; - private TestActionKeyCounter leftKeyCounter = null!; + private DefaultKeyCounter leftKeyCounter = null!; - private TestActionKeyCounter rightKeyCounter = null!; + private DefaultKeyCounter rightKeyCounter = null!; private OsuInputManager osuInputManager = null!; @@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests Origin = Anchor.Centre, Children = new Drawable[] { - leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) + leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton)) { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, Depth = float.MinValue, X = -100, }, - rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) + rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton)) { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, @@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertKeyCounter(int left, int right) { - AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); - AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); + AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left)); + AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right)); } private void releaseAllTouches() @@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); - public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler + public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler { public OsuAction Action { get; } - public TestActionKeyCounter(OsuAction action) + public TestActionKeyCounterTrigger(OsuAction action) : base(action.ToString()) { Action = action; @@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests { if (e.Action == Action) { - IsLit = true; - Increment(); + Activate(); } return false; @@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void OnReleased(KeyBindingReleaseEvent e) { - if (e.Action == Action) IsLit = false; + if (e.Action == Action) + Deactivate(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs index 371dfe6a1a..1de6b9ce55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs index 700a3f44bc..5569df8d95 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs index 4769e7660b..bf65a6c9d3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs index b7838ebaa7..661cc948c5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 0a1f5380b5..8b1a4f688c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.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 osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints protected override bool OnMouseDown(MouseDownEvent e) { - switch (e.Button) - { - case MouseButton.Left: - HitObject.Type = HitType.Centre; - EndPlacement(true); - return true; + if (e.Button != MouseButton.Left) + return false; - case MouseButton.Right: - HitObject.Type = HitType.Rim; - EndPlacement(true); - return true; - } - - return false; + EndPlacement(true); + return true; } public override void UpdateTimeAndPosition(SnapResult result) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs index 84aa5e6bba..f442435d9c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs index 89581c57bd..e517439ba4 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs index 68d6305fbf..9ef6fe8649 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs index 7cb14635ff..ad5da3d601 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 85d304da9c..518981980b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1,9 +1,8 @@ // 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.Diagnostics; using System.IO; using System.Linq; using NUnit.Framework; @@ -161,6 +160,21 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeImageSpecifiedAsVideo() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("image-specified-as-video.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + [Test] public void TestDecodeBeatmapTimingPoints() { @@ -320,6 +334,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var comboColors = decoder.Decode(stream).ComboColours; + Debug.Assert(comboColors != null); + Color4[] expectedColors = { new Color4(142, 199, 255, 255), @@ -330,7 +346,7 @@ namespace osu.Game.Tests.Beatmaps.Formats new Color4(255, 177, 140, 255), new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored. }; - Assert.AreEqual(expectedColors.Length, comboColors?.Count); + Assert.AreEqual(expectedColors.Length, comboColors.Count); for (int i = 0; i < expectedColors.Length; i++) Assert.AreEqual(expectedColors[i], comboColors[i]); } @@ -415,14 +431,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsNotNull(positionData); Assert.IsNotNull(curveData); - Assert.AreEqual(new Vector2(192, 168), positionData.Position); + Assert.AreEqual(new Vector2(192, 168), positionData!.Position); Assert.AreEqual(956, hitObjects[0].StartTime); Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); positionData = hitObjects[1] as IHasPosition; Assert.IsNotNull(positionData); - Assert.AreEqual(new Vector2(304, 56), positionData.Position); + Assert.AreEqual(new Vector2(304, 56), positionData!.Position); Assert.AreEqual(1285, hitObjects[1].StartTime); Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); } @@ -578,8 +594,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestFallbackDecoderForCorruptedHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -596,8 +612,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestFallbackDecoderForMissingHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("missing-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -614,8 +630,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithEmptyLinesAtStart() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -632,8 +648,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithEmptyLinesAndNoHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -650,8 +666,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithContentImmediatelyAfterHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -678,7 +694,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestAllowFallbackDecoderOverwrite() { - Decoder decoder = null; + Decoder decoder = null!; using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var stream = new LineBufferedReader(resStream)) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 281ea4e4ff..3a776ac225 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.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.Linq; using NUnit.Framework; using osuTK; @@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(storyboard.HasDrawable); Assert.AreEqual(6, storyboard.Layers.Count()); - StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); Assert.IsNotNull(background); Assert.AreEqual(16, background.Elements.Count); Assert.IsTrue(background.VisibleWhenFailing); Assert.IsTrue(background.VisibleWhenPassing); Assert.AreEqual("Background", background.Name); - StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); + StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2); Assert.IsNotNull(fail); Assert.AreEqual(0, fail.Elements.Count); Assert.IsTrue(fail.VisibleWhenFailing); Assert.IsFalse(fail.VisibleWhenPassing); Assert.AreEqual("Fail", fail.Name); - StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); + StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1); Assert.IsNotNull(pass); Assert.AreEqual(0, pass.Elements.Count); Assert.IsFalse(pass.VisibleWhenFailing); Assert.IsTrue(pass.VisibleWhenPassing); Assert.AreEqual("Pass", pass.Name); - StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); + StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0); Assert.IsNotNull(foreground); Assert.AreEqual(151, foreground.Elements.Count); Assert.IsTrue(foreground.VisibleWhenFailing); Assert.IsTrue(foreground.VisibleWhenPassing); Assert.AreEqual("Foreground", foreground.Name); - StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); + StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue); Assert.IsNotNull(overlay); Assert.IsEmpty(overlay.Elements); Assert.IsTrue(overlay.VisibleWhenFailing); @@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var sprite = background.Elements.ElementAt(0) as StoryboardSprite; Assert.NotNull(sprite); - Assert.IsTrue(sprite.HasCommands); + Assert.IsTrue(sprite!.HasCommands); Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.IsTrue(sprite.IsDrawable); Assert.AreEqual(Anchor.Centre, sprite.Origin); @@ -171,6 +169,21 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeImageSpecifiedAsVideo() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("image-specified-as-video.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(foreground.Elements.Count, Is.Zero); + } + } + [Test] public void TestDecodeOutOfRangeLoopAnimationType() { diff --git a/osu.Game.Tests/Resources/image-specified-as-video.osb b/osu.Game.Tests/Resources/image-specified-as-video.osb new file mode 100644 index 0000000000..9cea7dd4e7 --- /dev/null +++ b/osu.Game.Tests/Resources/image-specified-as-video.osb @@ -0,0 +1,4 @@ +osu file format v14 + +[Events] +Video,0,"BG.jpg",0,0 diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 5442b3bfef..f3f942b74b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2)); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 1dffeed01b..751aeb4e13 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index b918c5e64a..eecead5415 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [BackgroundDependencyLoader] private void load() @@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 890ac21b40..46d5e6c4d2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -7,7 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -17,28 +17,29 @@ namespace osu.Game.Tests.Visual.Gameplay { public TestSceneKeyCounter() { - KeyCounterKeyboard testCounter; - - KeyCounterDisplay kc = new KeyCounterDisplay + KeyCounterDisplay kc = new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Children = new KeyCounter[] - { - testCounter = new KeyCounterKeyboard(Key.X), - new KeyCounterKeyboard(Key.X), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right), - }, }; + kc.AddRange(new InputTrigger[] + { + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterMouseTrigger(MouseButton.Left), + new KeyCounterMouseTrigger(MouseButton.Right), + }); + + var testCounter = (DefaultKeyCounter)kc.Counters.First(); + AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - kc.Add(new KeyCounterKeyboard(key)); + kc.Add(new KeyCounterKeyboardTrigger(key)); }); - Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; + Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key; void addPressKeyStep() { @@ -46,12 +47,12 @@ namespace osu.Game.Tests.Visual.Gameplay } addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); - AddStep("Disable counting", () => testCounter.IsCounting = false); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); + AddStep("Disable counting", () => testCounter.IsCounting.Value = false); addPressKeyStep(); - AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); + AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); Add(kc); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 2ea27c2fef..dbd1ce1f6e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SessionStatics sessionStatics { get; set; } + [Resolved] + private OsuConfigManager config { get; set; } + [Cached(typeof(INotificationOverlay))] private readonly NotificationOverlay notificationOverlay; @@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay saveVolumes(); setFullVolume(); + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay restoreVolumes(); } + [Test] + public void TestEpilepsyWarningWithDisabledStoryboard() + { + saveVolumes(); + setFullVolume(); + + AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false)); + AddStep("change epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("epilepsy warning absent", () => getWarning() == null); + + restoreVolumes(); + } + [Test] public void TestEpilepsyWarningEarlyExit() { saveVolumes(); setFullVolume(); + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("click notification", () => notification.TriggerClick()); } - private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(w => w.IsAlive); private partial class TestPlayerLoader : PlayerLoader { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index c476aae202..bf9b13b320 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0)); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index a7da8f9832..93fec60de4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; return new Container diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 1f2329af4a..7bbfc6a62b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [Test] public void TestComboCounterIncrementing() @@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); action?.Invoke(hudOverlay); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 5c69062e67..3f78dbfd96 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.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.Collections.Generic; using System.Linq; @@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinnableSound : OsuTestScene { - private TestSkinSourceContainer skinSource; - private PausableSkinnableSound skinnableSound; + private TestSkinSourceContainer skinSource = null!; + private PausableSkinnableSound skinnableSound = null!; + + private const string sample_lookup = "Gameplay/normal-sliderslide"; [SetUpSteps] public void SetUpSteps() @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached. - skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide"))); + skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup))); }); } @@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample not playing", () => !skinnableSound.IsPlaying); } + [Test] + public void TestSampleUpdatedBeforePlaybackWhenNotPresent() + { + AddStep("make sample non-present", () => skinnableSound.Hide()); + AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False); + + AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo(sample_lookup)); + + AddStep("change source", () => + { + skinSource.OverridingSample = new SampleVirtual("new skin"); + skinSource.TriggerSourceChanged(); + }); + + AddStep("start sample", () => skinnableSound.Play()); + AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo("new skin")); + } + [Test] public void TestSkinChangeDoesntPlayOnPause() { - DrawableSample sample = null; + DrawableSample? sample = null; AddStep("start sample", () => { skinnableSound.Play(); @@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("retrieve and ensure current sample is different", () => { - DrawableSample oldSample = sample; + DrawableSample? oldSample = sample; sample = skinnableSound.ChildrenOfType().Single(); return sample != oldSample; }); @@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler { [Resolved] - private ISkinSource source { get; set; } + private ISkinSource source { get; set; } = null!; - public event Action SourceChanged; + public event Action? SourceChanged; public Bindable SamplePlaybackDisabled { get; } = new Bindable(); + public ISample? OverridingSample; + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled; - public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup); - public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); - public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); - public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); - public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); - public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty()); + public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup); + public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT); + public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo); + + public IBindable? GetConfig(TLookup lookup) + where TLookup : notnull + where TValue : notnull + { + return source.GetConfig(lookup); + } + + public ISkin? FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction); + public IEnumerable AllSources => new[] { this }.Concat(source.AllSources); public void TriggerSourceChanged() { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index d937b9e6d7..224e7e411e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); AddStep("press 'z'", () => InputManager.Key(Key.Z)); - AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0); + AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0); AddStep("press 's'", () => InputManager.Key(Key.S)); - AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); + AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1); } private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index eabc63b341..a9bdd21b64 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); break; + case LegacyEventType.Video: + string filename = CleanFilename(split[2]); + + // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO + // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported + // video extensions and handle similar to a background if it doesn't match. + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename))) + { + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + } + + break; + case LegacyEventType.Background: beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); break; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 44dbb3cc9f..f8308fe431 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats int offset = Parsing.ParseInt(split[1]); string path = CleanFilename(split[2]); + // See handling in LegacyBeatmapDecoder for the special case where a video type is used but + // the file extension is not a valid video. + // + // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video + // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path))) + break; + storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); break; } @@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); + timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, + startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); break; case "H": diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cf58d07b9e..8f27e5dc53 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -71,7 +71,7 @@ namespace osu.Game [Cached(typeof(OsuGameBase))] public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { - public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; + public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; public const string OSU_PROTOCOL = "osu://"; diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 9e4469bf25..733610c040 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -24,5 +24,22 @@ namespace osu.Game.Rulesets.Mods MaxValue = 2, Precision = 0.01, }; + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + // Each 0.1 multiple changes score multiplier by 0.02. + value /= 5; + + return 1 + value; + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 7d858dca6f..06c7750035 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -24,5 +24,19 @@ namespace osu.Game.Rulesets.Mods MaxValue = 0.99, Precision = 0.01, }; + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + return 1 + value; + } + } } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 64fe9c8a86..4f22c0c617 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 1d5fcc634e..76d84000f1 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.UI isDisposed = true; - if (ShaderManager.IsNotNull()) SampleStore.Dispose(); + if (SampleStore.IsNotNull()) SampleStore.Dispose(); if (TextureStore.IsNotNull()) TextureStore.Dispose(); if (ShaderManager.IsNotNull()) ShaderManager.Dispose(); } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7bf0482673..2ae54a3afe 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -19,7 +19,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.UI .Select(b => b.GetAction()) .Distinct() .OrderBy(action => action) - .Select(action => new KeyCounterAction(action))); + .Select(action => new KeyCounterActionTrigger(action))); } private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler @@ -179,11 +179,14 @@ namespace osu.Game.Rulesets.UI { } - public bool OnPressed(KeyBindingPressEvent e) => Target.Children.OfType>().Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); + public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger) + .Select(c => (KeyCounterActionTrigger)c.Trigger) + .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); public void OnReleased(KeyBindingReleaseEvent e) { - foreach (var c in Target.Children.OfType>()) + foreach (var c + in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger)) c.OnReleased(e.Action, Clock.Rate >= 0); } } diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 0214d33549..57bdad079e 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -1,15 +1,13 @@ // 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.Audio; -using osu.Framework.Bindables; -using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; using ManagedBass.Fx; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; +using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,6 +19,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -50,8 +49,7 @@ namespace osu.Game.Screens.Play private const float duration = 2500; - private ISample? failSample; - private SampleChannel? failSampleChannel; + private SkinnableSound failSample = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -76,10 +74,10 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, ISkinSource skin, IBindable beatmap) + private void load(AudioManager audio, IBindable beatmap) { track = beatmap.Value.Track; - failSample = skin.GetSample(new SampleInfo(@"Gameplay/failsound")); + AddInternal(failSample = new SkinnableSound(new SampleInfo("Gameplay/failsound"))); AddRangeInternal(new Drawable[] { @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Play failHighPassFilter.CutoffTo(300); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); - failSampleChannel = failSample?.Play(); + failSample.Play(); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); @@ -159,7 +157,7 @@ namespace osu.Game.Screens.Play /// public void Stop() { - failSampleChannel?.Stop(); + failSample.Stop(); removeFilters(); } diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs similarity index 70% rename from osu.Game/Screens/Play/KeyCounter.cs rename to osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs index 4405542b3b..69a3e53dfc 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,70 +11,23 @@ using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public abstract partial class KeyCounter : Container + public partial class DefaultKeyCounter : KeyCounter { - private Sprite buttonSprite; - private Sprite glowSprite; - private Container textLayer; - private SpriteText countSpriteText; - - public bool IsCounting { get; set; } = true; - private int countPresses; - - public int CountPresses - { - get => countPresses; - private set - { - if (countPresses != value) - { - countPresses = value; - countSpriteText.Text = value.ToString(@"#,0"); - } - } - } - - private bool isLit; - - public bool IsLit - { - get => isLit; - protected set - { - if (isLit != value) - { - isLit = value; - updateGlowSprite(value); - } - } - } - - public void Increment() - { - if (!IsCounting) - return; - - CountPresses++; - } - - public void Decrement() - { - if (!IsCounting) - return; - - CountPresses--; - } + private Sprite buttonSprite = null!; + private Sprite glowSprite = null!; + private Container textLayer = null!; + private SpriteText countSpriteText = null!; //further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray; public Color4 KeyUpTextColor { get; set; } = Color4.White; public double FadeTime { get; set; } - protected KeyCounter(string name) + public DefaultKeyCounter(InputTrigger trigger) + : base(trigger) { - Name = name; } [BackgroundDependencyLoader(true)] @@ -116,7 +67,7 @@ namespace osu.Game.Screens.Play }, countSpriteText = new OsuSpriteText { - Text = CountPresses.ToString(@"#,0"), + Text = CountPresses.Value.ToString(@"#,0"), Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativePositionAxes = Axes.Both, @@ -130,6 +81,9 @@ namespace osu.Game.Screens.Play // so the size can be changing between buttonSprite and glowSprite. Height = buttonSprite.DrawHeight; Width = buttonSprite.DrawWidth; + + IsActive.BindValueChanged(e => updateGlowSprite(e.NewValue), true); + CountPresses.BindValueChanged(e => countSpriteText.Text = e.NewValue.ToString(@"#,0"), true); } private void updateGlowSprite(bool show) diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs new file mode 100644 index 0000000000..14d7f56093 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -0,0 +1,83 @@ +// 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.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class DefaultKeyCounterDisplay : KeyCounterDisplay + { + private const int duration = 100; + private const double key_fade_time = 80; + + private readonly FillFlowContainer keyFlow; + + public override IEnumerable Counters => keyFlow; + + public DefaultKeyCounterDisplay() + { + InternalChild = keyFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Alpha = 0, + }; + } + + protected override void Update() + { + base.Update(); + + // Don't use autosize as it will shrink to zero when KeyFlow is hidden. + // In turn this can cause the display to be masked off screen and never become visible again. + Size = keyFlow.Size; + } + + public override void Add(InputTrigger trigger) => + keyFlow.Add(new DefaultKeyCounter(trigger) + { + FadeTime = key_fade_time, + KeyDownTextColor = KeyDownTextColor, + KeyUpTextColor = KeyUpTextColor, + }); + + protected override void UpdateVisibility() => + // Isolate changing visibility of the key counters from fading this component. + keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); + + private Color4 keyDownTextColor = Color4.DarkGray; + + public Color4 KeyDownTextColor + { + get => keyDownTextColor; + set + { + if (value != keyDownTextColor) + { + keyDownTextColor = value; + foreach (var child in keyFlow) + child.KeyDownTextColor = value; + } + } + } + + private Color4 keyUpTextColor = Color4.White; + + public Color4 KeyUpTextColor + { + get => keyUpTextColor; + set + { + if (value != keyUpTextColor) + { + keyUpTextColor = value; + foreach (var child in keyFlow) + child.KeyUpTextColor = value; + } + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs new file mode 100644 index 0000000000..b57f2cdf91 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/InputTrigger.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An event trigger which can be used with to create visual tracking of button/key presses. + /// + public abstract partial class InputTrigger : Component + { + /// + /// Callback to invoke when the associated input has been activated. + /// + /// Whether gameplay is progressing in the forward direction time-wise. + public delegate void OnActivateCallback(bool forwardPlayback); + + /// + /// Callback to invoke when the associated input has been deactivated. + /// + /// Whether gameplay is progressing in the forward direction time-wise. + public delegate void OnDeactivateCallback(bool forwardPlayback); + + public event OnActivateCallback? OnActivate; + public event OnDeactivateCallback? OnDeactivate; + + protected InputTrigger(string name) + { + Name = name; + } + + protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback); + + protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback); + } +} diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs new file mode 100644 index 0000000000..2a4ab1993a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -0,0 +1,98 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An individual key display which is intended to be displayed within a . + /// + public abstract partial class KeyCounter : Container + { + /// + /// The which activates and deactivates this . + /// + public readonly InputTrigger Trigger; + + /// + /// Whether the actions reported by should be counted. + /// + public Bindable IsCounting { get; } = new BindableBool(true); + + private readonly Bindable countPresses = new BindableInt + { + MinValue = 0 + }; + + /// + /// The current count of registered key presses. + /// + public IBindable CountPresses => countPresses; + + private readonly Container content; + + protected override Container Content => content; + + /// + /// Whether this is currently in the "activated" state because the associated key is currently pressed. + /// + protected readonly Bindable IsActive = new BindableBool(); + + protected KeyCounter(InputTrigger trigger) + { + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both + }, + Trigger = trigger, + }; + + Trigger.OnActivate += Activate; + Trigger.OnDeactivate += Deactivate; + + Name = trigger.Name; + } + + private void increment() + { + if (!IsCounting.Value) + return; + + countPresses.Value++; + } + + private void decrement() + { + if (!IsCounting.Value) + return; + + countPresses.Value--; + } + + protected virtual void Activate(bool forwardPlayback = true) + { + IsActive.Value = true; + if (forwardPlayback) + increment(); + } + + protected virtual void Deactivate(bool forwardPlayback = true) + { + IsActive.Value = false; + if (!forwardPlayback) + decrement(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Trigger.OnActivate -= Activate; + Trigger.OnDeactivate -= Deactivate; + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs similarity index 70% rename from osu.Game/Screens/Play/KeyCounterAction.cs rename to osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs index 900d9bcd0e..e5951a8bf4 100644 --- a/osu.Game/Screens/Play/KeyCounterAction.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs @@ -1,18 +1,16 @@ // 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.Collections.Generic; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterAction : KeyCounter + public partial class KeyCounterActionTrigger : InputTrigger where T : struct { public T Action { get; } - public KeyCounterAction(T action) + public KeyCounterActionTrigger(T action) : base($"B{(int)(object)action + 1}") { Action = action; @@ -23,9 +21,7 @@ namespace osu.Game.Screens.Play if (!EqualityComparer.Default.Equals(action, Action)) return false; - IsLit = true; - if (forwards) - Increment(); + Activate(forwards); return false; } @@ -34,9 +30,7 @@ namespace osu.Game.Screens.Play if (!EqualityComparer.Default.Equals(action, Action)) return; - IsLit = false; - if (!forwards) - Decrement(); + Deactivate(forwards); } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs new file mode 100644 index 0000000000..49c0da6793 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// A flowing display of all gameplay keys. Individual keys can be added using implementations. + /// + public abstract partial class KeyCounterDisplay : CompositeDrawable + { + /// + /// Whether the key counter should be visible regardless of the configuration value. + /// This is true by default, but can be changed. + /// + public Bindable AlwaysVisible { get; } = new Bindable(true); + + /// + /// The s contained in this . + /// + public abstract IEnumerable Counters { get; } + + /// + /// Whether the actions reported by all s within this should be counted. + /// + public Bindable IsCounting { get; } = new BindableBool(true); + + protected readonly Bindable ConfigVisibility = new Bindable(); + + protected abstract void UpdateVisibility(); + + private Receptor? receptor; + + public void SetReceptor(Receptor receptor) + { + if (this.receptor != null) + throw new InvalidOperationException("Cannot set a new receptor when one is already active"); + + this.receptor = receptor; + } + + /// + /// Add a to this display. + /// + public abstract void Add(InputTrigger trigger); + + /// + /// Add a range of to this display. + /// + public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AlwaysVisible.BindValueChanged(_ => UpdateVisibility()); + ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); + } + + public override bool HandleNonPositionalInput => receptor == null; + + public override bool HandlePositionalInput => receptor == null; + + public partial class Receptor : Drawable + { + protected readonly KeyCounterDisplay Target; + + public Receptor(KeyCounterDisplay target) + { + RelativeSizeAxes = Axes.Both; + Depth = float.MinValue; + Target = target; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool Handle(UIEvent e) + { + switch (e) + { + case KeyDownEvent: + case KeyUpEvent: + case MouseDownEvent: + case MouseUpEvent: + return Target.InternalChildren.Any(c => c.TriggerEvent(e)); + } + + return base.Handle(e); + } + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounterKeyboard.cs b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs similarity index 70% rename from osu.Game/Screens/Play/KeyCounterKeyboard.cs rename to osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs index c5c8b7eeae..3052c1e666 100644 --- a/osu.Game/Screens/Play/KeyCounterKeyboard.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs @@ -1,18 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osuTK.Input; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterKeyboard : KeyCounter + public partial class KeyCounterKeyboardTrigger : InputTrigger { public Key Key { get; } - public KeyCounterKeyboard(Key key) + public KeyCounterKeyboardTrigger(Key key) : base(key.ToString()) { Key = key; @@ -22,8 +20,7 @@ namespace osu.Game.Screens.Play { if (e.Key == Key) { - IsLit = true; - Increment(); + Activate(); } return base.OnKeyDown(e); @@ -31,7 +28,9 @@ namespace osu.Game.Screens.Play protected override void OnKeyUp(KeyUpEvent e) { - if (e.Key == Key) IsLit = false; + if (e.Key == Key) + Deactivate(); + base.OnKeyUp(e); } } diff --git a/osu.Game/Screens/Play/KeyCounterMouse.cs b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs similarity index 79% rename from osu.Game/Screens/Play/KeyCounterMouse.cs rename to osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs index cf9c7c029f..369aaa9f74 100644 --- a/osu.Game/Screens/Play/KeyCounterMouse.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.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. -#nullable disable - using osu.Framework.Input.Events; -using osuTK.Input; using osuTK; +using osuTK.Input; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterMouse : KeyCounter + public partial class KeyCounterMouseTrigger : InputTrigger { public MouseButton Button { get; } - public KeyCounterMouse(MouseButton button) + public KeyCounterMouseTrigger(MouseButton button) : base(getStringRepresentation(button)) { Button = button; @@ -39,17 +37,16 @@ namespace osu.Game.Screens.Play protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == Button) - { - IsLit = true; - Increment(); - } + Activate(); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { - if (e.Button == Button) IsLit = false; + if (e.Button == Button) + Deactivate(); + base.OnMouseUp(e); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index c8d06b82e8..9f050a07bd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -331,7 +331,7 @@ namespace osu.Game.Screens.Play ShowHealth = { BindTarget = ShowHealthBar } }; - protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay + protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs deleted file mode 100644 index bb50d4a539..0000000000 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play -{ - public partial class KeyCounterDisplay : Container - { - private const int duration = 100; - private const double key_fade_time = 80; - - private readonly Bindable configVisibility = new Bindable(); - - protected readonly FillFlowContainer KeyFlow; - - protected override Container Content => KeyFlow; - - /// - /// Whether the key counter should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - - public KeyCounterDisplay() - { - InternalChild = KeyFlow = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Alpha = 0, - }; - } - - protected override void Update() - { - base.Update(); - - // Don't use autosize as it will shrink to zero when KeyFlow is hidden. - // In turn this can cause the display to be masked off screen and never become visible again. - Size = KeyFlow.Size; - } - - public override void Add(KeyCounter key) - { - ArgumentNullException.ThrowIfNull(key); - - base.Add(key); - key.IsCounting = IsCounting; - key.FadeTime = key_fade_time; - key.KeyDownTextColor = KeyDownTextColor; - key.KeyUpTextColor = KeyUpTextColor; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.KeyOverlay, configVisibility); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AlwaysVisible.BindValueChanged(_ => updateVisibility()); - configVisibility.BindValueChanged(_ => updateVisibility(), true); - } - - private bool isCounting = true; - - public bool IsCounting - { - get => isCounting; - set - { - if (value == isCounting) return; - - isCounting = value; - foreach (var child in Children) - child.IsCounting = value; - } - } - - private Color4 keyDownTextColor = Color4.DarkGray; - - public Color4 KeyDownTextColor - { - get => keyDownTextColor; - set - { - if (value != keyDownTextColor) - { - keyDownTextColor = value; - foreach (var child in Children) - child.KeyDownTextColor = value; - } - } - } - - private Color4 keyUpTextColor = Color4.White; - - public Color4 KeyUpTextColor - { - get => keyUpTextColor; - set - { - if (value != keyUpTextColor) - { - keyUpTextColor = value; - foreach (var child in Children) - child.KeyUpTextColor = value; - } - } - } - - private void updateVisibility() => - // Isolate changing visibility of the key counters from fading this component. - KeyFlow.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); - - public override bool HandleNonPositionalInput => receptor == null; - public override bool HandlePositionalInput => receptor == null; - - private Receptor receptor; - - public void SetReceptor(Receptor receptor) - { - if (this.receptor != null) - throw new InvalidOperationException("Cannot set a new receptor when one is already active"); - - this.receptor = receptor; - } - - public partial class Receptor : Drawable - { - protected readonly KeyCounterDisplay Target; - - public Receptor(KeyCounterDisplay target) - { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Target = target; - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - protected override bool Handle(UIEvent e) - { - switch (e) - { - case KeyDownEvent: - case KeyUpEvent: - case MouseDownEvent: - case MouseUpEvent: - return Target.Children.Any(c => c.TriggerEvent(e)); - } - - return base.Handle(e); - } - } - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1f0f31b34e..5174adfc06 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -437,8 +437,11 @@ namespace osu.Game.Screens.Play }, KeyCounter = { + IsCounting = + { + Value = false + }, AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, - IsCounting = false }, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -478,7 +481,7 @@ namespace osu.Game.Screens.Play { updateGameplayState(); updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; + HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue; } private void updateGameplayState() diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4f7e4add32..30ae5ee5aa 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -67,6 +67,8 @@ namespace osu.Game.Screens.Play private OsuScrollContainer settingsScroll = null!; + private Bindable showStoryboards = null!; + private bool backgroundBrightnessReduction; private readonly BindableDouble volumeAdjustment = new BindableDouble(1); @@ -149,10 +151,11 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(SessionStatics sessionStatics, AudioManager audio) + private void load(SessionStatics sessionStatics, AudioManager audio, OsuConfigManager config) { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); + showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard); const float padding = 25; @@ -463,7 +466,10 @@ namespace osu.Game.Screens.Play // only show if the warning was created (i.e. the beatmap needs it) // and this is not a restart of the map (the warning expires after first load). - if (epilepsyWarning?.IsAlive == true) + // + // note the late check of storyboard enable as the user may have just changed it + // from the settings on the loader screen. + if (epilepsyWarning?.IsAlive == true && showStoryboards.Value) { const double epilepsy_display_length = 3000; @@ -483,6 +489,7 @@ namespace osu.Game.Screens.Play { // This goes hand-in-hand with the restoration of low pass filter in contentOut(). this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); + epilepsyWarning?.Expire(); } pushSequence.Schedule(() => diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 0158c47ea3..76c2c4d7ec 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -70,20 +69,6 @@ namespace osu.Game.Skinning updateSample(); } - protected override void LoadComplete() - { - base.LoadComplete(); - - CurrentSkin.SourceChanged += skinChangedImmediate; - } - - private void skinChangedImmediate() - { - // Clean up the previous sample immediately on a source change. - // This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled). - clearPreviousSamples(); - } - protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); @@ -109,6 +94,8 @@ namespace osu.Game.Skinning private void updateSample() { + clearPreviousSamples(); + if (sampleInfo == null) return; @@ -129,6 +116,8 @@ namespace osu.Game.Skinning /// public void Play() { + FlushPendingSkinChanges(); + if (Sample == null) return; @@ -172,14 +161,6 @@ namespace osu.Game.Skinning } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (CurrentSkin.IsNotNull()) - CurrentSkin.SourceChanged -= skinChangedImmediate; - } - #region Re-expose AudioContainer public BindableNumber Volume => sampleContainer.Volume; diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index cef1db4bc0..c7b33dc539 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Pooling; +using osu.Framework.Threading; namespace osu.Game.Skinning { @@ -14,6 +15,8 @@ namespace osu.Game.Skinning /// public abstract partial class SkinReloadableDrawable : PoolableDrawable { + private ScheduledDelegate? pendingSkinChange; + /// /// Invoked when has changed. /// @@ -31,21 +34,30 @@ namespace osu.Game.Skinning CurrentSkin.SourceChanged += onChange; } - private void onChange() => - // schedule required to avoid calls after disposed. - // note that this has the side-effect of components only performing a skin change when they are alive. - Scheduler.AddOnce(skinChanged); - protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); skinChanged(); } - private void skinChanged() + /// + /// Force any pending calls to be performed immediately. + /// + /// + /// When a skin change occurs, the handling provided by this class is scheduled. + /// In some cases, such a sample playback, this can result in the sample being played + /// just before it is updated to a potentially different sample. + /// + /// Calling this method will ensure any pending update operations are run immediately. + /// It is recommended to call this before consuming the result of skin changes for anything non-drawable. + /// + protected void FlushPendingSkinChanges() { - SkinChanged(CurrentSkin); - OnSkinChanged?.Invoke(); + if (pendingSkinChange == null) + return; + + pendingSkinChange.RunTask(); + pendingSkinChange = null; } /// @@ -56,6 +68,22 @@ namespace osu.Game.Skinning { } + private void onChange() + { + // schedule required to avoid calls after disposed. + // note that this has the side-effect of components only performing a skin change when they are alive. + pendingSkinChange?.Cancel(); + pendingSkinChange = Scheduler.Add(skinChanged); + } + + private void skinChanged() + { + SkinChanged(CurrentSkin); + OnSkinChanged?.Invoke(); + + pendingSkinChange = null; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 475b79053a..59b3799e0a 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -115,6 +115,8 @@ namespace osu.Game.Skinning /// public virtual void Play() { + FlushPendingSkinChanges(); + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)