diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png new file mode 100644 index 0000000000..5044c2d15f Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png new file mode 100644 index 0000000000..0a3d614739 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png new file mode 100644 index 0000000000..f0aa0b3a02 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png new file mode 100644 index 0000000000..8f9155b8f7 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 7c51036d69..3a9d465f8d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -4,6 +4,7 @@ Version: 2.5 [Mania] Keys: 4 ColumnLineWidth: 3,1,3,1,1 +LightFramePerSecond: 15 // some skins found in the wild had configuration keys where the @2x suffix was included in the values. // the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything // if @2x assets are present. @@ -15,5 +16,6 @@ Hit300: mania/hit300@2x Hit300g: mania/hit300g@2x StageLeft: mania/stage-left StageRight: mania/stage-right +StageLight: mania/stage-light NoteImage0L: LongNoteTailWang NoteImage1L: LongNoteTailWang diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 66e67136df..ee274fc45e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy direction.BindTo(scrollingInfo.Direction); isHitting.BindTo(holdNote.IsHitting); - bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d => + bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d => { if (d == null) return; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs index ab996519a7..914ed79234 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.UI.Scrolling; @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private readonly IBindable direction = new Bindable(); private Container lightContainer = null!; - private Sprite light = null!; + private Drawable light = null!; public LegacyColumnBackground() { @@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; + int lightFramePerSecond = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightFramePerSecond)?.Value ?? 60; + InternalChildren = new[] { lightContainer = new Container @@ -46,16 +47,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = lightPosition }, - Child = light = new Sprite + Child = light = skin.GetAnimation(lightImage, true, true, frameLength: 1000d / lightFramePerSecond)?.With(l => { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), - Texture = skin.GetTexture(lightImage), - RelativeSizeAxes = Axes.X, - Width = 1, - Alpha = 0 - } + l.Anchor = Anchor.BottomCentre; + l.Origin = Anchor.BottomCentre; + l.Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour); + l.RelativeSizeAxes = Axes.X; + l.Width = 1; + l.Alpha = 0; + }) ?? Empty(), } }; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 446dfae0f6..73c521b2ed 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy string filename = this.GetManiaSkinConfig(hit_result_mapping[result])?.Value ?? default_hit_result_skin_filenames[result]; - var animation = this.GetAnimation(filename, true, true); + var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); } diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs index baaa24959f..5ad268a77b 100644 --- a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests Child = piece = new TestLegacyMainCirclePiece(priorityLookup), }; - var sprites = this.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray(); + var sprites = this.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray(); Debug.Assert(sprites.Length <= 2); }); @@ -103,8 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestLegacyMainCirclePiece : LegacyMainCirclePiece { - public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); - public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); + public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); + public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); public TestLegacyMainCirclePiece(string? priorityLookupPrefix) : base(priorityLookupPrefix, false) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 3ec914596a..d8d86d1802 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -62,12 +63,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // otherwise fall back to the default prefix "hitcircle". string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle"; + Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2; + // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -76,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2)) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index a535fbdbc3..780084115d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -41,11 +42,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); - InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2) ?? Empty()).With(d => + InternalChild = arrow = new Sprite { - d.Anchor = Anchor.Centre; - d.Origin = Anchor.Centre; - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), + }; textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 4dce3f1d4a..dbc8718f02 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -48,40 +48,45 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider) { - Drawable? getDrawableFor(string lookup) + Drawable? getDrawableFor(string lookup, bool animatable) { const string normal_hit = "taikohit"; const string big_hit = "taikobig"; string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: max_circle_sprite_size) ?? + return skin.GetAnimation($"{prefix}{lookup}", animatable, false, maxSize: max_circle_sprite_size) ?? // fallback to regular size if "big" version doesn't exist. - skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: max_circle_sprite_size); + skin.GetAnimation($"{normal_hit}{lookup}", animatable, false, maxSize: max_circle_sprite_size); } // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. - AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle"))); + AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle", false)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + foregroundLayer = getDrawableFor("circleoverlay", true); - foregroundLayer = getDrawableFor("circleoverlay"); if (foregroundLayer != null) + { + foregroundLayer.Anchor = Anchor.Centre; + foregroundLayer.Origin = Anchor.Centre; + + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // For now just stop at first frame for sanity. + if (foregroundLayer is IFramedAnimation animatedForegroundLayer) + animatedForegroundLayer.Stop(); + AddInternal(foregroundLayer); + } drawableHitObject.StartTimeBindable.BindValueChanged(startTime => { timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT; }, true); - // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). - // For now just stop at first frame for sanity. - foreach (var c in InternalChildren) - { - (c as IFramedAnimation)?.Stop(); - - c.Anchor = Anchor.Centre; - c.Origin = Anchor.Centre; - } - if (gameplayState != null) currentCombo.BindTo(gameplayState.ScoreProcessor.Combo); } @@ -101,11 +106,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy foreach (var c in InternalChildren) c.Scale = new Vector2(DrawHeight / circle_piece_size.Y); - if (foregroundLayer is IFramedAnimation animatableForegroundLayer) - animateForegroundLayer(animatableForegroundLayer); + if (foregroundLayer is IFramedAnimation animatedForegroundLayer) + animateForegroundLayer(animatedForegroundLayer); } - private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer) + private void animateForegroundLayer(IFramedAnimation animation) { int multiplier; @@ -119,12 +124,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } else { - animatableForegroundLayer.GotoFrame(0); + animation.GotoFrame(0); return; } animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1; - animatableForegroundLayer.GotoFrame(animationFrame); + animation.GotoFrame(animationFrame); } private Color4 accentColour; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index f1c99a315d..9acb29a793 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -40,6 +40,7 @@ namespace osu.Game.Skinning public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public bool KeysUnderNotes; + public int LightFramePerSecond = 60; public LegacyNoteBodyStyle? NoteBodyStyle; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 784e57708e..cacca0de23 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -74,6 +74,7 @@ namespace osu.Game.Skinning Hit50, Hit0, KeysUnderNotes, - NoteBodyStyle + NoteBodyStyle, + LightFramePerSecond } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index e880e3c1ed..b472afb74f 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -123,6 +123,11 @@ namespace osu.Game.Skinning currentConfig.WidthForNoteHeightScale = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; + case "LightFramePerSecond": + int lightFramePerSecond = int.Parse(pair.Value, CultureInfo.InvariantCulture); + currentConfig.LightFramePerSecond = lightFramePerSecond > 0 ? lightFramePerSecond : 24; + break; + case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal): HandleColours(currentConfig, line, true); break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 76190d0abe..dc683f1dae 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -273,6 +273,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); + + case LegacyManiaSkinConfigurationLookups.LightFramePerSecond: + return SkinUtils.As(new Bindable(existing.LightFramePerSecond)); } return null;