// 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.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning { public static partial class LegacySkinExtensions { 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); 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) { if (source == null) return null; 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); var animation = new SkinnableTextureAnimation(startAtCurrentTime) { DefaultFrameLength = frameLength ?? getFrameLength(retrievalSource, applyConfigFrameRate, textures), Loop = looping, }; foreach (var t in textures) 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. var singleTexture = retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); return singleTexture != null ? new[] { singleTexture } : Array.Empty(); IEnumerable getTextures(ISkin skin) { for (int i = 0; true; i++) { Texture? texture; if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null) break; yield return texture; } } string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}"; } public static bool HasFont(this ISkin source, LegacyFont font) { return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null; } public static string GetFontPrefix(this ISkin source, LegacyFont font) { switch (font) { case LegacyFont.Score: return source.GetConfig(LegacySetting.ScorePrefix)?.Value ?? "score"; case LegacyFont.Combo: return source.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; case LegacyFont.HitCircle: return source.GetConfig(LegacySetting.HitCirclePrefix)?.Value ?? "default"; default: throw new ArgumentOutOfRangeException(nameof(font)); } } /// /// Returns the numeric overlap of number sprites to use. /// A positive number will bring the number sprites closer together, while a negative number /// will split them apart more. /// public static float GetFontOverlap(this ISkin source, LegacyFont font) { switch (font) { case LegacyFont.Score: return source.GetConfig(LegacySetting.ScoreOverlap)?.Value ?? 0f; case LegacyFont.Combo: return source.GetConfig(LegacySetting.ComboOverlap)?.Value ?? 0f; case LegacyFont.HitCircle: return source.GetConfig(LegacySetting.HitCircleOverlap)?.Value ?? -2f; default: throw new ArgumentOutOfRangeException(nameof(font)); } } public partial class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] private IAnimationTimeReference? timeReference { get; set; } private readonly Bindable animationStartTime = new BindableDouble(); public SkinnableTextureAnimation(bool startAtCurrentTime = true) : base(startAtCurrentTime) { } protected override void LoadComplete() { base.LoadComplete(); if (timeReference != null) { Clock = timeReference.Clock; animationStartTime.BindTo(timeReference.AnimationStartTime); } animationStartTime.BindValueChanged(_ => updatePlaybackPosition(), true); } private void updatePlaybackPosition() { if (timeReference == null) return; PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime.Value; } } private const double default_frame_time = 1000 / 60d; private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures) { if (applyConfigFrameRate) { var iniRate = source.GetConfig(LegacySetting.AnimationFramerate); if (iniRate?.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; } return default_frame_time; } } }