diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 34e6d1996d..b2f4fa2738 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -7,11 +7,10 @@ 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; using osu.Game.Rulesets.Osu; -using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; using osuTK; @@ -36,7 +35,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 +48,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 +107,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)); } } 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(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 2ef6f28720..8a14b8b183 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -2,10 +2,10 @@ // 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; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Skinning; @@ -13,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; } @@ -90,25 +90,52 @@ 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; } [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++) + { + AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay); + } + } + else if (storyboard.UseSkinSprites) + { + // fallback to skin if required. + skin.SourceChanged += skinSourceChanged; + skinSourceChanged(); } Animation.ApplyTransforms(this); } + + private void skinSourceChanged() + { + ClearFrames(); + + // 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(texture, Animation.FrameDelay); + } + + private string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin != null) + skin.SourceChanged -= skinSourceChanged; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index db10f13896..a6f2b8fcbd 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,33 @@ 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); + + if (skin != null) + 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; } } }