From 0b74bde653c2e8be85b4467b8e3ed3db5fe4f5ed Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 13 Mar 2022 08:21:52 +0300 Subject: [PATCH 01/20] Disable taiko playfield aspect ratio lock on "Classic" mod --- osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs | 1 + osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 7 ++++++- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 14 ++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 6520517039..5a6f57bc36 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; + drawableTaikoRuleset.LockPlayfieldAspect.Value = false; } public void Update(Playfield playfield) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 824b95639b..2efc4176f5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.UI { public new BindableDouble TimeRange => base.TimeRange; + public readonly BindableBool LockPlayfieldAspect = new BindableBool(true); + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override bool UserScrollSpeedAdjustment => false; @@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Taiko.UI return ControlPoints[result]; } - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer + { + LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect } + }; protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 1041456020..9cf530e903 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.UI; -using osuTK; namespace osu.Game.Rulesets.Taiko.UI { @@ -13,16 +13,22 @@ namespace osu.Game.Rulesets.Taiko.UI private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_aspect = 16f / 9f; + public readonly IBindable LockPlayfieldAspect = new BindableBool(true); + protected override void Update() { base.Update(); - float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; - Size = new Vector2(1, default_relative_height * aspectAdjust); + float height = default_relative_height; + + if (LockPlayfieldAspect.Value) + height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; + + Height = height; // Position the taiko playfield exactly one playfield from the top of the screen. RelativePositionAxes = Axes.Y; - Y = Size.Y; + Y = height; } } } From 4a8aa72d1ba96dcb60f545250800ef71437a4cf3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 01:03:08 +0300 Subject: [PATCH 02/20] Nudge osu!taiko playfield's height to perfectly match with osu!(stable) --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d650cab729..504b10e9bc 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// /// Default height of a when inside a . /// - public const float DEFAULT_HEIGHT = 212; + public const float DEFAULT_HEIGHT = 200; private Container hitExplosionContainer; private Container kiaiExplosionContainer; From dbc26c3534e37108497ae64e040ba32101988fa6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 03:46:05 +0300 Subject: [PATCH 03/20] Add failing test assertion --- .../Gameplay/TestSceneDrawableStoryboardSprite.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 52bedc328d..4011a6bc82 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -1,11 +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 System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Skinning; @@ -22,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private Storyboard storyboard { get; set; } = new Storyboard(); + private IEnumerable sprites => this.ChildrenOfType(); + [Test] public void TestSkinSpriteDisallowedByDefault() { @@ -41,9 +45,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); - AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.Centre, Vector2.Zero))); + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); assertSpritesFromSkin(true); + + AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType().Single().Size, new Vector2(128, 128)))); } private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) @@ -57,7 +63,6 @@ namespace osu.Game.Tests.Visual.Gameplay private void assertSpritesFromSkin(bool fromSkin) => AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}", - () => this.ChildrenOfType() - .All(sprite => sprite.ChildrenOfType().Any() == fromSkin)); + () => sprites.All(sprite => sprite.ChildrenOfType().Any() == fromSkin)); } } From c9d54834be09429edc2a96a99ea579e15ac83870 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 03:35:08 +0300 Subject: [PATCH 04/20] Fix `SkinnableSprite`s in storyboards not autosizing to their textures --- osu.Game/Storyboards/Storyboard.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 2faed98ae0..b662b98e4e 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -104,7 +104,13 @@ namespace osu.Game.Storyboards drawable = new Sprite { Texture = textureStore.Get(storyboardPath) }; // if the texture isn't available locally in the beatmap, some storyboards choose to source from the underlying skin lookup hierarchy. else if (UseSkinSprites) - drawable = new SkinnableSprite(path); + { + drawable = new SkinnableSprite(path) + { + RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, + }; + } return drawable; } From 720e1cd206154fc40f8889ef813f1e31d01d1e0a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 03:35:23 +0300 Subject: [PATCH 05/20] Add failing test cases --- .../TestSceneDrawableStoryboardSprite.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 4011a6bc82..34e6d1996d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; @@ -52,6 +53,49 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType().Single().Size, new Vector2(128, 128)))); } + [Test] + public void TestFlippedSprite() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + AddStep("flip sprites", () => sprites.ForEach(s => + { + s.FlipH = true; + s.FlipV = true; + })); + AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); + } + + [Test] + public void TestNegativeScale() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); + AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); + } + + [Test] + public void TestNegativeScaleWithFlippedSprite() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); + AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); + AddStep("flip sprites", () => sprites.ForEach(s => + { + s.FlipH = true; + s.FlipV = true; + })); + AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft)); + } + private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) => new DrawableStoryboardSprite( new StoryboardSprite(lookupName, origin, initialPosition) From 0b8c89bfa81c88019f629c8941d542f15d06fc6d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 03:50:12 +0300 Subject: [PATCH 06/20] Fix drawable storyboard sprites not flipping origin on negative scale --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index eb877f3dff..ad4402dcc4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -79,7 +79,7 @@ namespace osu.Game.Storyboards.Drawables { var origin = base.Origin; - if (FlipH) + if ((FlipH || VectorScale.X < 0) && !(FlipH && VectorScale.X < 0)) { if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); @@ -87,7 +87,7 @@ namespace osu.Game.Storyboards.Drawables origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } - if (FlipV) + if ((FlipV || VectorScale.Y < 0) && !(FlipV && VectorScale.Y < 0)) { if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); From 9cf05080da2bb0125eaf3483326d1ab4fa0ab66c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 04:40:24 +0300 Subject: [PATCH 07/20] Simplify conditionals to one XOR operations with comments --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ad4402dcc4..f3173497e8 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -79,7 +79,8 @@ namespace osu.Game.Storyboards.Drawables { var origin = base.Origin; - if ((FlipH || VectorScale.X < 0) && !(FlipH && VectorScale.X < 0)) + // Either flip horizontally or negative X scale, but not both. + if (FlipH ^ (VectorScale.X < 0)) { if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); @@ -87,7 +88,8 @@ namespace osu.Game.Storyboards.Drawables origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } - if ((FlipV || VectorScale.Y < 0) && !(FlipV && VectorScale.Y < 0)) + // Either flip vertically or negative Y scale, but not both. + if (FlipV ^ (VectorScale.Y < 0)) { if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); From 740a72e16d07b380177c331096780043ded35da1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 05:44:34 +0300 Subject: [PATCH 08/20] Share origin adjustment logic between storyboard sprite and animation --- .../Drawables/DrawableStoryboardAnimation.cs | 30 +------------ osu.Game/Storyboards/StoryboardExtensions.cs | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Storyboards/StoryboardExtensions.cs diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 81623a9307..28ac83a25c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; @@ -70,34 +69,9 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; - protected override Vector2 DrawScale - => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; + protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; - public override Anchor Origin - { - get - { - var origin = base.Origin; - - if (FlipH) - { - if (origin.HasFlagFast(Anchor.x0)) - origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlagFast(Anchor.x2)) - origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - } - - if (FlipV) - { - if (origin.HasFlagFast(Anchor.y0)) - origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlagFast(Anchor.y2)) - origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - } - - return origin; - } - } + public override Anchor Origin => StoryboardExtensions.AdjustOrigin(base.Origin, VectorScale, FlipH, FlipV); public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; diff --git a/osu.Game/Storyboards/StoryboardExtensions.cs b/osu.Game/Storyboards/StoryboardExtensions.cs new file mode 100644 index 0000000000..4e8251c9e7 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics; +using osuTK; + +namespace osu.Game.Storyboards +{ + public static class StoryboardExtensions + { + /// + /// Given an origin and a set of properties, adjust the origin to display the sprite/animation correctly. + /// + /// The current origin. + /// The vector scale. + /// Whether the element is flipped horizontally. + /// Whether the element is flipped vertically. + /// The adjusted origin. + public static Anchor AdjustOrigin(Anchor origin, Vector2 vectorScale, bool flipH, bool flipV) + { + // Either flip horizontally or negative X scale, but not both. + if (flipH ^ (vectorScale.X < 0)) + { + if (origin.HasFlagFast(Anchor.x0)) + origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + else if (origin.HasFlagFast(Anchor.x2)) + origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + } + + // Either flip vertically or negative Y scale, but not both. + if (flipV ^ (vectorScale.Y < 0)) + { + if (origin.HasFlagFast(Anchor.y0)) + origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + else if (origin.HasFlagFast(Anchor.y2)) + origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + } + + return origin; + } + } +} From c1697c7621358e02dccb710d79842696a983c669 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 14 Mar 2022 06:29:49 +0300 Subject: [PATCH 09/20] Update `DrawableStoryboardSprite` to use helper method --- .../Drawables/DrawableStoryboardAnimation.cs | 3 +- .../Drawables/DrawableStoryboardSprite.cs | 29 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 28ac83a25c..88cb5f40a1 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -69,7 +69,8 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; - protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; + protected override Vector2 DrawScale + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; public override Anchor Origin => StoryboardExtensions.AdjustOrigin(base.Origin, VectorScale, FlipH, FlipV); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index f3173497e8..db10f13896 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -73,33 +72,7 @@ namespace osu.Game.Storyboards.Drawables protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; - public override Anchor Origin - { - get - { - var origin = base.Origin; - - // Either flip horizontally or negative X scale, but not both. - if (FlipH ^ (VectorScale.X < 0)) - { - if (origin.HasFlagFast(Anchor.x0)) - origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlagFast(Anchor.x2)) - origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - } - - // Either flip vertically or negative Y scale, but not both. - if (FlipV ^ (VectorScale.Y < 0)) - { - if (origin.HasFlagFast(Anchor.y0)) - origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlagFast(Anchor.y2)) - origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - } - - return origin; - } - } + public override Anchor Origin => StoryboardExtensions.AdjustOrigin(base.Origin, VectorScale, FlipH, FlipV); public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; From 4ae6cba080d707402399b295f4a41b9503545991 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Mar 2022 13:49:38 +0900 Subject: [PATCH 10/20] Expose UseDevelopmentServer as virtual --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8b75de9718..602f84b0e1 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -69,7 +69,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public bool UseDevelopmentServer { get; } + public virtual bool UseDevelopmentServer { get; } public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); From 21beb8774d5d3d95bbcd19e524e571e181512048 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Mar 2022 13:54:54 +0900 Subject: [PATCH 11/20] Change to lambda method --- osu.Game/OsuGameBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 602f84b0e1..5468db348e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -69,7 +69,7 @@ namespace osu.Game /// private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer { get; } + public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); @@ -177,7 +177,6 @@ namespace osu.Game public OsuGameBase() { - UseDevelopmentServer = DebugUtils.IsDebugBuild; Name = @"osu!"; } From 8676a2587c12c2a200d6122d53736a611401e1a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 15:45:43 +0900 Subject: [PATCH 12/20] Add the ability for `HitObject`s to specify auxiliary samples --- osu.Game/Rulesets/Objects/HitObject.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index c590cc302f..57b897e5b5 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; @@ -67,6 +68,12 @@ namespace osu.Game.Rulesets.Objects } } + /// + /// Any samples which may be used by this hit object that are non-standard. + /// This is used only to preload these samples ahead of time. + /// + public virtual IList AuxiliarySamples => ImmutableList.Empty; + public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT; public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT; From 90e34d7686b9a9762ba153ca841c5ad685497f3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 15:45:57 +0900 Subject: [PATCH 13/20] Move slider slide samples to auxiliary specification --- .../Objects/Drawables/DrawableSlider.cs | 22 +++++-------------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 17 ++++++++++++++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3acec4498d..1447f131c6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,23 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; -using osuTK; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; -using osuTK.Graphics; using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -126,18 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); - - var slidingSamples = new List(); - - var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); - if (normalSample != null) - slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide")); - - var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); - if (whistleSample != null) - slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle")); - - slidingSample.Samples = slidingSamples.ToArray(); + slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); } public override void StopAllSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5c1c3fd253..601311694d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -29,6 +29,23 @@ namespace osu.Game.Rulesets.Osu.Objects set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } + public override IList AuxiliarySamples => CreateSlidingSamples(); + + public IList CreateSlidingSamples() + { + var slidingSamples = new List(); + + var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + if (normalSample != null) + slidingSamples.Add(normalSample.With("sliderslide")); + + var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + if (whistleSample != null) + slidingSamples.Add(whistleSample.With("sliderwhistle")); + + return slidingSamples; + } + private readonly Cached endPositionCache = new Cached(); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); From be9920218805cc3280fb1e16d3816774305ca069 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:08:04 +0900 Subject: [PATCH 14/20] Move spinner spin samples to auxiliary specification --- .../Objects/Drawables/DrawableSpinner.cs | 11 ++--------- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index c6db02ee02..a904658a4c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -121,15 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - var firstSample = HitObject.Samples.FirstOrDefault(); - - if (firstSample != null) - { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin"); - - spinningSample.Samples = new ISampleInfo[] { clone }; - spinningSample.Frequency.Value = spinning_sample_initial_frequency; - } + spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + spinningSample.Frequency.Value = spinning_sample_initial_frequency; } private void updateSpinningSample(ValueChangedEvent tracking) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 1eddfb7fef..ddee4d3ebd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -1,7 +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; +using System.Collections.Generic; +using System.Linq; using System.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -73,5 +77,20 @@ namespace osu.Game.Rulesets.Osu.Objects public override Judgement CreateJudgement() => new OsuJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override IList AuxiliarySamples => CreateSpinningSamples(); + + public HitSampleInfo[] CreateSpinningSamples() + { + var referenceSample = Samples.FirstOrDefault(); + + if (referenceSample == null) + return Array.Empty(); + + return new[] + { + SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin") + }; + } } } From 6d6f73e0168d6c7d5c447e0b32c6d8566d5b4761 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:08:26 +0900 Subject: [PATCH 15/20] Add overrides in `DrawableSliderTail` to explain/warn that this class never plays its own samples --- .../Objects/Drawables/DrawableSliderTail.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index ec1387eb54..64964ed396 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -74,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } + protected override void LoadSamples() + { + // Tail models don't actually get samples, as the playback is handled by DrawableSlider. + // This override is only here for visibility in explaining this weird flow. + } + + public override void PlaySamples() + { + // Tail models don't actually get samples, as the playback is handled by DrawableSlider. + // This override is only here for visibility in explaining this weird flow. + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); From 45233932089ea26d9e329c47f1893137d2d4bcaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:08:57 +0900 Subject: [PATCH 16/20] Remove `IsLayered` from `LegacyHitSampleInfo` comparison The equality of samples is generally used to compare the sample equality, not its full properties. For instance, we don't compare `Volume` in the base implementation. Having `IsLayered` here breaks actual usages of equality, ie. for pooling purposes. --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b091803406..47fe9032f0 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -500,7 +500,7 @@ namespace osu.Game.Rulesets.Objects.Legacy => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) - => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; + => base.Equals(other) && CustomSampleBank == other.CustomSampleBank; public override bool Equals(object? obj) => obj is LegacyHitSampleInfo other && Equals(other); From 1b8c632b878b6d5c30a7a5693dc5834d0aa987b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:17:14 +0900 Subject: [PATCH 17/20] Add `TailSamples` to auxiliary samples list --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 601311694d..776165cfb4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public override IList AuxiliarySamples => CreateSlidingSamples(); + public override IList AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray(); public IList CreateSlidingSamples() { From 39d95aa8cf412b1a5ef66e1cd1d05541fef4f3bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:19:23 +0900 Subject: [PATCH 18/20] Add automatic preloading of sample pools at a `Playfield` level --- osu.Game/Rulesets/UI/Playfield.cs | 73 +++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 30e71dde1c..cd8f99db8b 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -3,22 +3,22 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Drawables; 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.Graphics.Pooling; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; -using System.Diagnostics; namespace osu.Game.Rulesets.UI { @@ -264,10 +264,25 @@ namespace osu.Game.Rulesets.UI var entry = CreateLifetimeEntry(hitObject); lifetimeEntryMap[entry.HitObject] = entry; + preloadSamples(hitObject); + HitObjectContainer.Add(entry); OnHitObjectAdded(entry.HitObject); } + private void preloadSamples(HitObject hitObject) + { + // prepare sample pools ahead of time so we're not initialising at runtime. + foreach (var sample in hitObject.Samples) + prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); + + foreach (var sample in hitObject.AuxiliarySamples) + prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); + + foreach (var nestedObject in hitObject.NestedHitObjects) + preloadSamples(nestedObject); + } + /// /// Removes a for a pooled from this . /// @@ -330,22 +345,7 @@ namespace osu.Game.Rulesets.UI DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent) { - var lookupType = hitObject.GetType(); - - IDrawablePool pool; - - // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. - if (!pools.TryGetValue(lookupType, out pool)) - { - foreach (var (t, p) in pools) - { - if (!t.IsInstanceOfType(hitObject)) - continue; - - pools[lookupType] = pool = p; - break; - } - } + var pool = prepareDrawableHitObjectPool(hitObject); return (DrawableHitObject)pool?.Get(d => { @@ -372,14 +372,39 @@ namespace osu.Game.Rulesets.UI }); } + private IDrawablePool prepareDrawableHitObjectPool(HitObject hitObject) + { + var lookupType = hitObject.GetType(); + + IDrawablePool pool; + + // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. + if (!pools.TryGetValue(lookupType, out pool)) + { + foreach (var (t, p) in pools) + { + if (!t.IsInstanceOfType(hitObject)) + continue; + + pools[lookupType] = pool = p; + break; + } + } + + return pool; + } + private readonly Dictionary> samplePools = new Dictionary>(); - public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) - { - if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1)); + public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) => prepareSamplePool(sampleInfo).Get(); - return existingPool.Get(); + private DrawablePool prepareSamplePool(ISampleInfo sampleInfo) + { + if (samplePools.TryGetValue(sampleInfo, out var pool)) return pool; + + AddInternal(samplePools[sampleInfo] = pool = new DrawableSamplePool(sampleInfo, 1)); + + return pool; } private class DrawableSamplePool : DrawablePool From 3c5fda5f23fce4a384cf286b4cab2ff180cfaa33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 17:24:31 +0900 Subject: [PATCH 19/20] Add early exist if the target screen is no longer current --- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index abd8272633..73af2591c8 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -132,6 +132,9 @@ namespace osu.Game.Skinning.Editor { Debug.Assert(skinEditor != null); + if (!target.IsCurrentScreen()) + return; + if (!target.IsLoaded) { Scheduler.AddOnce(setTarget, target); From 16ee6b5fc7700b2a46359fb6b814e9c56eafaa53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Mar 2022 18:12:04 +0900 Subject: [PATCH 20/20] Remove `IsLayered` from `GetHasCode` implementation --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 47fe9032f0..8dc037c7c8 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public override bool Equals(object? obj) => obj is LegacyHitSampleInfo other && Equals(other); - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank); } private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable