From 93bdca5211c52c37c5e0fc358693affe9366ad5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Apr 2022 17:26:23 +0900 Subject: [PATCH 1/6] Split out `GetTextures` helper function for `LegacySkin`s --- osu.Game/Skinning/LegacySkinExtensions.cs | 86 ++++++++++++++--------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 479afabb00..514a06a4ee 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,39 +20,32 @@ namespace osu.Game.Skinning { public static class LegacySkinExtensions { - [CanBeNull] - public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = true, double? frameLength = null) + public static Drawable? GetAnimation(this ISkin? source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", + bool startAtCurrentTime = true, double? frameLength = null) => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength); - [CanBeNull] - public static Drawable GetAnimation(this ISkin source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, - string animationSeparator = "-", - bool startAtCurrentTime = true, double? frameLength = null) + public static Drawable? GetAnimation(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, + string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null) { - Texture texture; - - // find the first source which provides either the animated or non-animated version. - ISkin skin = (source as ISkinSource)?.FindProvider(s => - { - if (animatable && s.GetTexture(getFrameName(0)) != null) - return true; - - return s.GetTexture(componentName, wrapModeS, wrapModeT) != null; - }) ?? source; - - if (skin == null) + if (source == null) return null; - if (animatable) - { - var textures = getTextures().ToArray(); + var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, out var retrievalSource); + + switch (textures.Length) + { + case 0: + return null; + + case 1: + return new Sprite { Texture = textures[0] }; + + default: + Debug.Assert(retrievalSource != null); - if (textures.Length > 0) - { var animation = new SkinnableTextureAnimation(startAtCurrentTime) { - DefaultFrameLength = frameLength ?? getFrameLength(skin, applyConfigFrameRate, textures), + DefaultFrameLength = frameLength ?? getFrameLength(retrievalSource, applyConfigFrameRate, textures), Loop = looping, }; @@ -58,19 +53,46 @@ namespace osu.Game.Skinning animation.AddFrame(t); return animation; - } + } + } + + public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, out ISkin? retrievalSource) + { + retrievalSource = null; + + if (source == null) + return Array.Empty(); + + // find the first source which provides either the animated or non-animated version. + retrievalSource = (source as ISkinSource)?.FindProvider(s => + { + if (animatable && s.GetTexture(getFrameName(0)) != null) + return true; + + return s.GetTexture(componentName, wrapModeS, wrapModeT) != null; + }) ?? source; + + if (animatable) + { + var textures = getTextures(retrievalSource).ToArray(); + + if (textures.Length > 0) + return textures; } // if an animation was not allowed or not found, fall back to a sprite retrieval. - if ((texture = skin.GetTexture(componentName, wrapModeS, wrapModeT)) != null) - return new Sprite { Texture = texture }; + var singleTexture = retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); - return null; + return singleTexture != null + ? new[] { singleTexture } + : Array.Empty(); - IEnumerable getTextures() + IEnumerable getTextures(ISkin skin) { for (int i = 0; true; i++) { + Texture? texture; + if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null) break; @@ -130,7 +152,7 @@ namespace osu.Game.Skinning public class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] - private IAnimationTimeReference timeReference { get; set; } + private IAnimationTimeReference? timeReference { get; set; } private readonly Bindable animationStartTime = new BindableDouble(); From cbc4e5319d1fa8d7b0f77ca9a5549cc56acaedad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Apr 2022 17:26:31 +0900 Subject: [PATCH 2/6] Fix `DrawableStoryboardAnimation` to handle skin fallback frame count similar to stable Reasoning is explained in the inline comment (basically, stable doesn't care what the user specifies as the frame count when falling back to skin resources). This change also removes on to two layers of drawables, which should be a win in heavy storyboards. --- .../Drawables/DrawableStoryboardAnimation.cs | 47 +++++++++++++++++-- .../Drawables/DrawableStoryboardSprite.cs | 27 ++++++++--- osu.Game/Storyboards/Storyboard.cs | 20 ++------ 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 2ef6f28720..28e65a7910 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; @@ -98,17 +99,55 @@ namespace osu.Game.Storyboards.Drawables return new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); } + [Resolved] + private ISkinSource skin { get; set; } + [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) { - for (int frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) + int frameIndex = 0; + + Texture frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore); + + if (frameTexture != null) { - string framePath = Animation.Path.Replace(".", frameIndex + "."); - Drawable frame = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Empty(); - AddFrame(frame, Animation.FrameDelay); + // sourcing from storyboard. + for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) + { + frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore); + AddFrame(new Sprite { Texture = frameTexture }, Animation.FrameDelay); + } + } + else if (storyboard.UseSkinSprites) + { + // fallback to skin if required. + skin.SourceChanged += skinSourceChanged; + skinSourceChanged(); } Animation.ApplyTransforms(this); } + + private void skinSourceChanged() + { + ClearFrames(); + + // ClearFrames doesn't clear the last displayed frame. + // Clear manually for now, in case the skin doesn't provide any frames. + DisplayFrame(Empty()); + + // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored + // and resources are retrieved until the end of the animation. + foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path), default, default, true, string.Empty, out _)) + AddFrame(new Sprite { Texture = texture }, Animation.FrameDelay); + } + + private string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + skin.SourceChanged -= skinSourceChanged; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index db10f13896..6622cfb6be 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -4,14 +4,15 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSprite : CompositeDrawable, IFlippable, IVectorScalable + public class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; } @@ -85,19 +86,31 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sprite.StartTime; LifetimeEnd = sprite.EndTime; - - AutoSizeAxes = Axes.Both; } + [Resolved] + private ISkinSource skin { get; set; } + [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) { - var drawable = storyboard.CreateSpriteFromResourcePath(Sprite.Path, textureStore); + Texture = storyboard.GetTextureFromPath(Sprite.Path, textureStore); - if (drawable != null) - InternalChild = drawable; + if (Texture == null && storyboard.UseSkinSprites) + { + skin.SourceChanged += skinSourceChanged; + skinSourceChanged(); + } Sprite.ApplyTransforms(this); } + + private void skinSourceChanged() => Texture = skin.GetTexture(Sprite.Path); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + skin.SourceChanged -= skinSourceChanged; + } } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index b662b98e4e..1d21b5dce2 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -4,13 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets.Mods; -using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards @@ -94,25 +91,14 @@ namespace osu.Game.Storyboards public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) => new DrawableStoryboard(this, mods); - public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) + public Texture GetTextureFromPath(string path, TextureStore textureStore) { - Drawable drawable = null; - string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); if (!string.IsNullOrEmpty(storyboardPath)) - 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) - { - RelativeSizeAxes = Axes.None, - AutoSizeAxes = Axes.Both, - }; - } + return textureStore.Get(storyboardPath); - return drawable; + return null; } } } From 040afff670d37335aaa5b8f5aab4ec21af087517 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Apr 2022 17:34:50 +0900 Subject: [PATCH 3/6] Change `DrawableStoryboardAnimation` to derive from `TextureAnimation` --- .../Drawables/DrawableStoryboardAnimation.cs | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 28e65a7910..5ebca4d860 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -6,7 +6,6 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Skinning; @@ -14,7 +13,7 @@ using osuTK; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardAnimation : DrawableAnimation, IFlippable, IVectorScalable + public class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; } @@ -91,14 +90,6 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = animation.EndTime; } - protected override Vector2 GetCurrentDisplaySize() - { - Texture texture = (CurrentFrame as Sprite)?.Texture - ?? ((CurrentFrame as SkinnableSprite)?.Drawable as Sprite)?.Texture; - - return new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); - } - [Resolved] private ISkinSource skin { get; set; } @@ -114,8 +105,7 @@ namespace osu.Game.Storyboards.Drawables // sourcing from storyboard. for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) { - frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore); - AddFrame(new Sprite { Texture = frameTexture }, Animation.FrameDelay); + AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay); } } else if (storyboard.UseSkinSprites) @@ -132,14 +122,10 @@ namespace osu.Game.Storyboards.Drawables { ClearFrames(); - // ClearFrames doesn't clear the last displayed frame. - // Clear manually for now, in case the skin doesn't provide any frames. - DisplayFrame(Empty()); - // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path), default, default, true, string.Empty, out _)) - AddFrame(new Sprite { Texture = texture }, Animation.FrameDelay); + AddFrame(texture, Animation.FrameDelay); } private string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); From 0674862b6c69ab7df325fa9d7506deee79b36aae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Apr 2022 17:51:10 +0900 Subject: [PATCH 4/6] Fix failing tests --- .../Gameplay/TestSceneDrawableStoryboardSprite.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 34e6d1996d..cfe9e35298 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets; @@ -36,7 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); - assertSpritesFromSkin(false); + AddAssert("sprite didn't find texture", () => + sprites.All(sprite => sprite.ChildrenOfType().All(s => s.Texture == null))); } [Test] @@ -48,9 +50,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); - assertSpritesFromSkin(true); + // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture. + AddAssert("sprite found texture", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); - AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType().Single().Size, new Vector2(128, 128)))); + AddAssert("skinnable sprite has correct size", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128)))); } [Test] @@ -104,9 +109,5 @@ namespace osu.Game.Tests.Visual.Gameplay s.LifetimeStart = double.MinValue; s.LifetimeEnd = double.MaxValue; }); - - private void assertSpritesFromSkin(bool fromSkin) => - AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}", - () => sprites.All(sprite => sprite.ChildrenOfType().Any() == fromSkin)); } } From 04db80848bade5ee16f46621339cf4c5622bbd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Apr 2022 21:24:50 +0200 Subject: [PATCH 5/6] Remove unused using directives --- .../Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index cfe9e35298..b2f4fa2738 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -9,10 +9,8 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; using osuTK; From 71c0216c559449c8286a816d0d592ebaad41fdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Apr 2022 21:37:42 +0200 Subject: [PATCH 6/6] Add null check guards to unsubscriptions in `Dispose()` --- osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs | 4 +++- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 5ebca4d860..8a14b8b183 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -133,7 +133,9 @@ namespace osu.Game.Storyboards.Drawables protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - skin.SourceChanged -= skinSourceChanged; + + if (skin != null) + skin.SourceChanged -= skinSourceChanged; } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 6622cfb6be..a6f2b8fcbd 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -110,7 +110,9 @@ namespace osu.Game.Storyboards.Drawables protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - skin.SourceChanged -= skinSourceChanged; + + if (skin != null) + skin.SourceChanged -= skinSourceChanged; } } }