diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index d929da1a29..ea2f031d65 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { @@ -61,7 +62,12 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (lookup) { case CatchSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + if (result == null) + return null; + + result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); + return (IBindable)result; } return Source.GetConfig(lookup); diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5be54d3882..381d066750 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning colouredSprite = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 957743c5f1..b22687a0a7 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] + [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index ff4865c71d..8ba58e3af3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; - public ColumnTestContainer(int column, ManiaAction action) + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { - this.column = new Column(column) + InternalChildren = new[] { - Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd - }; - - InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) - { - RelativeSizeAxes = Axes.Both + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, + Alpha = showColumn ? 1 : 0 + }, + content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }, + this.column.TopLevelContainer.CreateProxy() }; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 18eebada00..d24c81dac6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ColumnTestContainer(0, ManiaAction.Key1) + new ColumnTestContainer(0, ManiaAction.Key1, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning })); }) }, - new ColumnTestContainer(1, ManiaAction.Key2) + new ColumnTestContainer(1, ManiaAction.Key2, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2795868c97..37b34d1721 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); + + if (mods.HasFlag(LegacyMods.Mirror)) + yield return new ManiaModMirror(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -175,6 +178,10 @@ namespace osu.Game.Rulesets.Mania case ManiaModFadeIn _: value |= LegacyMods.FadeIn; break; + + case ManiaModMirror _: + value |= LegacyMods.Mirror; + break; } } @@ -326,6 +333,16 @@ namespace osu.Game.Rulesets.Mania Height = 250 }), } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) + } } }; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a04e5bc2f9..2ebcc5451a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -238,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) + { ApplyResult(r => r.Type = HitResult.Perfect); + endHold(); + } if (Tail.Result.Type == HitResult.Miss) HasBroken = true; diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs new file mode 100644 index 0000000000..c8b05ed2f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs @@ -0,0 +1,46 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9f716428c0..c0f0fcb4af 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -1,6 +1,8 @@ // 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); - private Drawable sprite; + [CanBeNull] + private Drawable bodySprite; + + [CanBeNull] + private Drawable lightContainer; + + [CanBeNull] + private Drawable light; public LegacyBodyPiece() { @@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => + string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value + ?? "lightingL"; + + float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value + ?? 1; + + // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. + // This animation is discarded and re-queried with the appropriate frame length afterwards. + var tmp = skin.GetAnimation(lightImage, true, false); + double frameLength = 0; + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + { + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(lightScale); + }); + + if (light != null) + { + lightContainer = new HitTargetInsetContainer + { + Alpha = 0, + Child = light + }; + } + + bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; @@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Wrap }); - if (sprite != null) - InternalChild = sprite; + if (bodySprite != null) + InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); @@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning private void onIsHittingChanged(ValueChangedEvent isHitting) { - if (!(sprite is TextureAnimation animation)) + if (bodySprite is TextureAnimation bodyAnimation) + { + bodyAnimation.GotoFrame(0); + bodyAnimation.IsPlaying = isHitting.NewValue; + } + + if (lightContainer == null) return; - animation.GotoFrame(0); - animation.IsPlaying = isHitting.NewValue; + if (isHitting.NewValue) + { + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); + + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); + + lightContainer.FadeIn(80); + } + else + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); + } } private void onDirectionChanged(ValueChangedEvent direction) { - if (sprite == null) - return; - if (direction.NewValue == ScrollingDirection.Up) { - sprite.Origin = Anchor.BottomCentre; - sprite.Scale = new Vector2(1, -1); + if (bodySprite != null) + { + bodySprite.Origin = Anchor.BottomCentre; + bodySprite.Scale = new Vector2(1, -1); + } + + if (light != null) + light.Anchor = Anchor.TopCentre; } else { - sprite.Origin = Anchor.TopCentre; - sprite.Scale = Vector2.One; + if (bodySprite != null) + { + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Scale = Vector2.One; + } + + if (light != null) + light.Anchor = Anchor.BottomCentre; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + lightContainer?.Expire(); + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index acae4cd6fb..3bf51b3073 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 1e1a9c2237..6eced571d2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour), Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 44f3e7d7b3..b269ea25d4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false) + Column.UnderlayElements.Add(CreateProxy()); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 19ec86b1ed..b0bab8e760 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -2,14 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -108,75 +106,43 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Container + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box { - RelativeSizeAxes = Axes.Both, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - }, + RelativeSizeAxes = Axes.Both + }, backgroundColour), new HitTargetInsetContainer { RelativeSizeAxes = Axes.Both, Children = new[] { - new Box + new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 + Alpha = hasLeftLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, - new Box + new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 + Alpha = hasRightLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, } } }; } } - - private class HitTargetInsetContainer : Container - { - private readonly IBindable direction = new Bindable(); - - protected override Container Content => content; - private readonly Container content; - - private float hitPosition; - - public HitTargetInsetContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin, IScrollingInfo scrollingInfo) - { - hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; - - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - content.Padding = direction.NewValue == ScrollingDirection.Up - ? new MarginPadding { Top = hitPosition } - : new MarginPadding { Bottom = hitPosition }; - } - } } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index f4b00ec476..e7a2de266d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -79,7 +79,6 @@ namespace osu.Game.Rulesets.Mania.UI columnFlow = new ColumnFlow(definition) { RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container { diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png new file mode 100644 index 0000000000..4f50f638c5 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png new file mode 100644 index 0000000000..daf28e09cb Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png new file mode 100644 index 0000000000..6ef1068420 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 3ff37c4147..03d18cefef 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Tests this.hasColours = hasColours; } - protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + protected override IBeatmapSkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); } private class TestBeatmapSkin : LegacyBeatmapSkin diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 075bf314bc..64da80a88e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneSkinFallbacks : TestSceneOsuPlayer { private readonly TestSource testUserSkin; - private readonly TestSource testBeatmapSkin; + private readonly BeatmapTestSource testBeatmapSkin; public TestSceneSkinFallbacks() { testUserSkin = new TestSource("user"); - testBeatmapSkin = new TestSource("beatmap"); + testBeatmapSkin = new BeatmapTestSource(); } [Test] @@ -80,15 +80,15 @@ namespace osu.Game.Rulesets.Osu.Tests public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly ISkinSource skin; + private readonly IBeatmapSkin skin; - public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) + public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, IBeatmapSkin skin) : base(beatmap, storyboard, frameBasedClock, audio) { this.skin = skin; } - protected override ISkin GetSkin() => skin; + protected override IBeatmapSkin GetSkin() => skin; } public class SkinProvidingPlayer : TestPlayer @@ -112,6 +112,14 @@ namespace osu.Game.Rulesets.Osu.Tests } } + private class BeatmapTestSource : TestSource, IBeatmapSkin + { + public BeatmapTestSource() + : base("beatmap") + { + } + } + public class TestSource : ISkinSource { private readonly string identifier; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 47b3926ceb..94d1cb8864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests drawableSpinner = new TestDrawableSpinner(spinner, auto) { Anchor = Anchor.Centre, - Depth = depthIndex++ + Depth = depthIndex++, + Scale = new Vector2(0.75f) }; foreach (var mod in SelectedMods.Value.OfType()) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eaa5d8937a..f527eb2312 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] + new StatisticItem("Timing Distribution", + new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0ab3e8825b..d15a0a3203 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite = new Sprite { Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 81a0df5ea5..e157842fd1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning { private DrawableSpinner drawableSpinner; private Sprite disc; + private Sprite metreSprite; private Container metre; - private const float background_y_offset = 20; - private const float sprite_scale = 1 / 1.6f; + private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) @@ -35,50 +35,58 @@ namespace osu.Game.Rulesets.Osu.Skinning RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChild = new Container { - new Sprite + // the old-style spinner relied heavily on absolute screen-space coordinate values. + // wrap everything in a container simulating absolute coords to preserve alignment + // as there are skins that depend on it. + Width = 640, + Height = 480, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Texture = source.GetTexture("spinner-background"), - Y = background_y_offset, - Scale = new Vector2(sprite_scale) - }, - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(sprite_scale) - }, - metre = new Container - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Y = background_y_offset, - Masking = true, - Child = new Sprite + new Sprite { - Texture = source.GetTexture("spinner-metre"), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background"), + Scale = new Vector2(sprite_scale) }, - Scale = new Vector2(0.625f) + disc = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(sprite_scale) + }, + metre = new Container + { + AutoSizeAxes = Axes.Both, + // this anchor makes no sense, but that's what stable uses. + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + // adjustment for stable (metre has additional offset) + Margin = new MarginPadding { Top = 20 }, + Masking = true, + Child = metreSprite = new Sprite + { + Texture = source.GetTexture("spinner-metre"), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Scale = new Vector2(0.625f) + } + } } }; } - private Vector2 metreFinalSize; - protected override void LoadComplete() { base.LoadComplete(); this.FadeOut(); drawableSpinner.State.BindValueChanged(updateStateTransforms, true); - - metreFinalSize = metre.Size = metre.Child.Size; } private void updateStateTransforms(ValueChangedEvent state) @@ -93,7 +101,16 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Update(); disc.Rotation = drawableSpinner.RotationTracker.Rotation; - metre.Height = getMetreHeight(drawableSpinner.Progress); + + // careful: need to call this exactly once for all calculations in a frame + // as the function has a random factor in it + var metreHeight = getMetreHeight(drawableSpinner.Progress); + + // hack to make the metre blink up from below than down from above. + // move down the container to be able to apply masking for the metre, + // and then move the sprite back up the same amount to keep its position absolute. + metre.Y = final_metre_height - metreHeight; + metreSprite.Y = -metre.Y; } private const int total_bars = 10; @@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning if (RNG.NextBool(((int)progress % 10) / 10f)) barCount++; - return (float)barCount / total_bars * metreFinalSize.Y; + return (float)barCount / total_bars * final_metre_height; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 0f586034d5..25ab96445a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.With(d => + LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; - }), + }), ballColour), layerSpec = new Sprite { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index bfcf268c3d..9b73ccd248 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour; + backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8223e3bc01..5ab8e3a8c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour; - body.Colour = accentColour; - end.Colour = accentColour; + var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + + headCircle.AccentColour = colour; + body.Colour = colour; + end.Colour = colour; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 656728f6e4..b11b64c22c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = component == TaikoSkinComponents.CentreHit - ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255); + AccentColour = LegacyColourCompatibility.DisallowZeroAlpha( + component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255)); } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2011842591..dbc32f2c3e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs new file mode 100644 index 0000000000..0f6d956b3c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class BeatmapDifficultyManagerTest + { + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs new file mode 100644 index 0000000000..9c71466489 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -0,0 +1,70 @@ +// 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.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyScoreDecoderTest + { + [Test] + public void TestDecodeManiaReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID); + + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); + + Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + + private class TestLegacyScoreDecoder : LegacyScoreDecoder + { + private static readonly Dictionary rulesets = new Ruleset[] + { + new OsuRuleset(), + new TaikoRuleset(), + new CatchRuleset(), + new ManiaRuleset() + }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; + + protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + MD5Hash = md5Hash, + Ruleset = new OsuRuleset().RulesetInfo, + BaseDifficulty = new BeatmapDifficulty() + } + }); + } + } +} diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index b30870d057..bc9528beb6 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } - private class TestSkin : LegacySkin + private class TestSkin : LegacySkin, IBeatmapSkin { public TestSkin(string resourceName, AudioManager audioManager) : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Gameplay this.audio = audio; } - protected override ISkin GetSkin() => new TestSkin("test-sample", audio); + protected override IBeatmapSkin GetSkin() => new TestSkin("test-sample", audio); } private class TestDrawableStoryboardSample : DrawableStoryboardSample diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr new file mode 100644 index 0000000000..da1a7bdd28 Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/mania-replay.osr differ diff --git a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini deleted file mode 100644 index 3c0dae6b13..0000000000 --- a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini +++ /dev/null @@ -1,5 +0,0 @@ -[General] -Version: latest - -[Colours] -Combo1: 255,255,255,0 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index c408d2f182..aedf26ee75 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -108,15 +108,5 @@ namespace osu.Game.Tests.Skins using (var stream = new LineBufferedReader(resStream)) Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); } - - [Test] - public void TestDecodeColourWithZeroAlpha() - { - var decoder = new LegacySkinDecoder(); - - using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini")) - using (var stream = new LineBufferedReader(resStream)) - Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f)); - } } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 74808bc2f5..03cb5fa3db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -212,6 +213,25 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); } + [Test] + public void TestDownloadButtonInitiallyDisabled() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + + AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value); + + AddStep("click contracted panel", () => + { + var contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X); + InputManager.MoveMouseTo(contractedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -255,6 +275,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); score.TotalScore += 10 - i; + score.Hash = $"test{i}"; scores.Add(score); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs similarity index 88% rename from osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs index aa569e47b1..07a0bcc8d8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneSimpleStatisticRow : OsuTestScene + public class TestSceneSimpleStatisticTable : OsuTestScene { private Container container; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestEmpty() { AddStep("create with no items", - () => container.Add(new SimpleStatisticRow(2, Enumerable.Empty()))); + () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty()))); } [Test] @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking Value = RNG.Next(100) }); - container.Add(new SimpleStatisticRow(columnCount, items)); + container.Add(new SimpleStatisticTable(columnCount, items)); }); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b80b4e45ed..0100c9b210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => + { + // Computation may have finished in a previous task. + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + return existing; + + return computeDifficulty(key, beatmapInfo, rulesetInfo); + }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps updateScheduler?.Dispose(); } - private readonly struct DifficultyCacheLookup : IEquatable + public readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; public readonly int RulesetId; @@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => BeatmapId == other.BeatmapId && RulesetId == other.RulesetId - && Mods.SequenceEqual(other.Mods); + && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); public override int GetHashCode() { diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 39c5ccab27..44728cc251 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps return storyboard; } - protected override ISkin GetSkin() + protected override IBeatmapSkin GetSkin() { try { diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 44ef9bcacc..c15240a4f6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats try { byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255; - - if (alpha == 0) - alpha = 255; - colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha); } catch diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 31975157a0..aac41725a9 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -42,9 +42,9 @@ namespace osu.Game.Beatmaps Storyboard Storyboard { get; } /// - /// Retrieves the which this provides. + /// Retrieves the which this provides. /// - ISkin Skin { get; } + IBeatmapSkin Skin { get; } /// /// Constructs a playable from using the applicable converters for a specific . diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 583e950e49..0e517ea3df 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + Mirror = 1 << 30, } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index b4bcf285b9..163b62a55c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); - skin = new RecyclableLazy(GetSkin); + skin = new RecyclableLazy(GetSkin); total_count.Value++; } @@ -275,10 +275,10 @@ namespace osu.Game.Beatmaps private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; - public ISkin Skin => skin.Value; + public IBeatmapSkin Skin => skin.Value; - protected virtual ISkin GetSkin() => new DefaultSkin(); - private readonly RecyclableLazy skin; + protected virtual IBeatmapSkin GetSkin() => new DefaultBeatmapSkin(); + private readonly RecyclableLazy skin; /// /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6af2561c89..648e4a762b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + if (args.Length > 3 && args[1] == "osu.ppy.sh") { switch (args[2]) { diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a4a560c8e4..97cb5ca7ab 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; using SharpCompress.Compressors.LZMA; @@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy protected void CalculateAccuracy(ScoreInfo score) { - score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); - score.Statistics.TryGetValue(HitResult.Meh, out int count50); - score.Statistics.TryGetValue(HitResult.Good, out int count100); - score.Statistics.TryGetValue(HitResult.Great, out int count300); - score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); - score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); + int countMiss = score.GetCountMiss() ?? 0; + int count50 = score.GetCount50() ?? 0; + int count100 = score.GetCount100() ?? 0; + int count300 = score.GetCount300() ?? 0; + int countGeki = score.GetCountGeki() ?? 0; + int countKatu = score.GetCountKatu() ?? 0; switch (score.Ruleset.ID) { @@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy } var diff = Parsing.ParseFloat(split[0]); + var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; - if (i == 0 && diff == 0) - // osu-stable adds a zero-time frame before potentially valid negative user frames. - // we need to ignore this. + if (i < 2 && mouseX == 256 && mouseY == -500) + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; // Todo: At some point we probably want to rewind and play back the negative-time frames @@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, - Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), - Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), + mouseX, + mouseY, (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); replay.Frames.Add(currentFrame); diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 6731fef6f7..d92d38da45 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + smallRing.Delay(400).FadeColour(Color4.Black, 300); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index d0142e57fe..b76842f405 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -74,23 +74,33 @@ namespace osu.Game.Screens.Ranking { button.State.Value = state.NewValue; - switch (replayAvailability) - { - case ReplayAvailability.Local: - button.TooltipText = @"watch replay"; - break; - - case ReplayAvailability.Online: - button.TooltipText = @"download replay"; - break; - - default: - button.TooltipText = @"replay unavailable"; - break; - } + updateTooltip(); }, true); - button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + Model.BindValueChanged(_ => + { + button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + + updateTooltip(); + }, true); + } + + private void updateTooltip() + { + switch (replayAvailability) + { + case ReplayAvailability.Local: + button.TooltipText = @"watch replay"; + break; + + case ReplayAvailability.Online: + button.TooltipText = @"download replay"; + break; + + default: + button.TooltipText = @"replay unavailable"; + break; + } } private enum ReplayAvailability diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index e6c4ab1c4e..3d9ba2f225 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -41,13 +41,14 @@ namespace osu.Game.Screens.Ranking.Statistics { Text = Name, Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Torus.With(weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) } }); } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs similarity index 93% rename from osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs rename to osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 16501aae54..8b503cc04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,10 +14,10 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Represents a statistic row with simple statistics (ones that only need textual display). + /// Represents a table with simple statistics (ones that only need textual display). /// Richer visualisations should be done with s and s. /// - public class SimpleStatisticRow : CompositeDrawable + public class SimpleStatisticTable : CompositeDrawable { private readonly SimpleStatisticItem[] items; private readonly int columnCount; @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The number of columns to layout the into. /// The s to display in this row. - public SimpleStatisticRow(int columnCount, IEnumerable items) + public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) { if (columnCount < 1) throw new ArgumentOutOfRangeException(nameof(columnCount)); diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index ed98698411..485d24d024 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - new Drawable[] + new[] { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 9, - Width = 4, - Colour = Color4Extensions.FromHex("#00FFAA") - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - } - } - } + createHeader(item) }, new Drawable[] { @@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } + + private static Drawable createHeader(StatisticItem item) + { + if (string.IsNullOrEmpty(item.Name)) + return Empty(); + + return new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = item.Name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + }; + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e959ed24fc..4903983759 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. + /// The name of the item. Can be to hide the item header. /// The content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 7f406331cd..c2ace6a04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), + Alpha = 0 }; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { rows.Add(new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] @@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); + d.FadeIn(250, Easing.OutQuint); }, localCancellationSource.Token); }), localCancellationSource.Token); } diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs new file mode 100644 index 0000000000..5b368c3e8d --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -0,0 +1,39 @@ +// 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 osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class UnstableRate : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public UnstableRate(IEnumerable hitEvents) + : base("Unstable Rate") + { + var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray(); + Value = 10 * standardDeviation(timeOffsets); + } + + private static double standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return double.NaN; + + var mean = timeOffsets.Average(); + var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + + protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + } +} diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 40335db697..346bfe53b8 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Skinning /// /// A container which overrides existing skin options with beatmap-local values. /// - public class BeatmapSkinProvidingContainer : SkinProvidingContainer + public class BeatmapSkinProvidingContainer : SkinProvidingContainer, IBeatmapSkin { private readonly Bindable beatmapSkins = new Bindable(); private readonly Bindable beatmapHitsounds = new Bindable(); @@ -21,7 +21,7 @@ namespace osu.Game.Skinning protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value; protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value; - public BeatmapSkinProvidingContainer(ISkin skin) + public BeatmapSkinProvidingContainer(IBeatmapSkin skin) : base(skin) { } diff --git a/osu.Game/Skinning/DefaultBeatmapSkin.cs b/osu.Game/Skinning/DefaultBeatmapSkin.cs new file mode 100644 index 0000000000..7b5ccd45c3 --- /dev/null +++ b/osu.Game/Skinning/DefaultBeatmapSkin.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public class DefaultBeatmapSkin : DefaultSkin, IBeatmapSkin + { + } +} diff --git a/osu.Game/Skinning/IBeatmapSkin.cs b/osu.Game/Skinning/IBeatmapSkin.cs new file mode 100644 index 0000000000..91caaed557 --- /dev/null +++ b/osu.Game/Skinning/IBeatmapSkin.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + /// + /// Marker interface for skins that originate from beatmaps. + /// + public interface IBeatmapSkin : ISkin + { + } +} diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d647bc4a2d..d53349dd11 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning { - public class LegacyBeatmapSkin : LegacySkin + public class LegacyBeatmapSkin : LegacySkin, IBeatmapSkin { protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; diff --git a/osu.Game/Skinning/LegacyColourCompatibility.cs b/osu.Game/Skinning/LegacyColourCompatibility.cs new file mode 100644 index 0000000000..b842b50426 --- /dev/null +++ b/osu.Game/Skinning/LegacyColourCompatibility.cs @@ -0,0 +1,46 @@ +// 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.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. + /// + public static class LegacyColourCompatibility + { + /// + /// Forces an alpha of 1 if a given is fully transparent. + /// + /// + /// This is equivalent to setting colour post-constructor in osu!stable. + /// + /// The to disallow zero alpha on. + /// The resultant . + public static Color4 DisallowZeroAlpha(Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + /// + /// Applies a to a , doubling the alpha value into the property. + /// + /// + /// This is equivalent to setting colour in the constructor in osu!stable. + /// + /// The to apply the colour to. + /// The to apply. + /// The given . + public static T ApplyWithDoubledAlpha(T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = DisallowZeroAlpha(colour); + return drawable; + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..65d5851455 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -31,10 +31,12 @@ namespace osu.Game.Skinning public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; public readonly float[] ExplosionWidth; + public readonly float[] HoldNoteLightWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; + public bool KeysUnderNotes; public LegacyManiaSkinConfiguration(int keys) { @@ -44,6 +46,7 @@ namespace osu.Game.Skinning ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; ExplosionWidth = new float[keys]; + HoldNoteLightWidth = new float[keys]; ColumnLineWidth.AsSpan().Fill(2); ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 4990ca8e60..a99710ea96 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,6 +34,8 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + HoldNoteLightImage, + HoldNoteLightScale, ExplosionImage, ExplosionScale, ColumnLineColour, @@ -50,5 +52,6 @@ namespace osu.Game.Skinning Hit100, Hit50, Hit0, + KeysUnderNotes, } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index aebc229f7c..a9d88e77ad 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -97,10 +97,18 @@ namespace osu.Game.Skinning currentConfig.ShowJudgementLine = pair.Value == "1"; break; + case "KeysUnderNotes": + currentConfig.KeysUnderNotes = pair.Value == "1"; + break; + case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + case "LightingLWidth": + parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth); + break; + case "WidthForNoteHeightScale": float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; if (minWidth > 0) @@ -116,6 +124,7 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): case string _ when pair.Key.StartsWith("Stage"): + case string _ when pair.Key.StartsWith("Lighting"): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 02d07eee45..5caf07b554 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -173,6 +173,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + case LegacyManiaSkinConfigurationLookups.ExplosionImage: + return SkinUtils.As(getManiaImage(existing, "LightingN")); + case LegacyManiaSkinConfigurationLookups.ExplosionScale: Debug.Assert(maniaLookup.TargetColumn != null); @@ -217,6 +220,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: + return SkinUtils.As(getManiaImage(existing, "LightingL")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); @@ -255,6 +272,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.Hit300: case LegacyManiaSkinConfigurationLookups.Hit300g: return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); } return null; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index ab4fb38657..db080d889f 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Beatmaps this.resourceStore = resourceStore; } - protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + protected override IBeatmapSkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); } } }