diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 8f3f39be9b..4c1ba33aa2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests if (withModifiedSkin) { AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f)); - AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget)); + AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("exit player", () => Player.Exit()); CreateTest(); } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index e8231b07ad..f60ae29f77 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestCatcherHyperStateReverted() { - DrawableCatchHitObject drawableObject1 = null; - DrawableCatchHitObject drawableObject2 = null; JudgementResult result1 = null; JudgementResult result2 = null; AddStep("catch hyper fruit", () => { - attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1); + result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }); }); AddStep("catch normal fruit", () => { - attemptCatch(new Fruit(), out drawableObject2, out result2); + result2 = attemptCatch(new Fruit()); }); AddStep("revert second result", () => { - catcher.OnRevertResult(drawableObject2, result2); + catcher.OnRevertResult(result2); }); checkHyperDash(true); AddStep("revert first result", () => { - catcher.OnRevertResult(drawableObject1, result1); + catcher.OnRevertResult(result1); }); checkHyperDash(false); } @@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestCatcherAnimationStateReverted() { - DrawableCatchHitObject drawableObject = null; JudgementResult result = null; AddStep("catch kiai fruit", () => { - attemptCatch(new TestKiaiFruit(), out drawableObject, out result); + result = attemptCatch(new TestKiaiFruit()); }); checkState(CatcherAnimationState.Kiai); AddStep("revert result", () => { - catcher.OnRevertResult(drawableObject, result); + catcher.OnRevertResult(result); }); checkState(CatcherAnimationState.Idle); } @@ -268,23 +265,19 @@ namespace osu.Game.Rulesets.Catch.Tests private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state); - private void attemptCatch(CatchHitObject hitObject) - { - attemptCatch(() => hitObject, 1); - } - private void attemptCatch(Func hitObject, int count) { for (int i = 0; i < count; i++) - attemptCatch(hitObject(), out _, out _); + attemptCatch(hitObject()); } - private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result) + private JudgementResult attemptCatch(CatchHitObject hitObject) { hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableObject = createDrawableObject(hitObject); - result = createResult(hitObject); + var drawableObject = createDrawableObject(hitObject); + var result = createResult(hitObject); applyResult(drawableObject, result); + return result; } private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index a06435583b..fb8af9bdb6 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -28,11 +28,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is GlobalSkinComponentLookup targetComponent) + if (lookup is SkinComponentsContainerLookup containerLookup) { - switch (targetComponent.Lookup) + switch (containerLookup.Target) { - case GlobalSkinComponentLookup.LookupType.MainHUDComponents: + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var components = base.GetDrawableComponent(lookup) as Container; if (providesComboCounter && components != null) diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index dbbe905879..3d0062d32f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value); } - public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) + public void OnRevertResult(JudgementResult result) { if (!result.Type.AffectsCombo() || !result.HasResult) return; - updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); + updateCombo(result.ComboAtJudgement, null); } private void updateCombo(int newCombo, Color4? hitObjectColour) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index c33d021876..cf7337fd0d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); - private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) - => CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result); + private void onRevertResult(JudgementResult result) + => CatcherArea.OnRevertResult(result); } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 411330f6fc..1c52c092ec 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Catch.UI } } - public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result) + public void OnRevertResult(JudgementResult result) { var catchResult = (CatchJudgementResult)result; @@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); } - caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); - droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); + caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false); + droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false); } /// diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 4f7535d13a..1b99270b65 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.OnNewResult(hitObject, result); } - public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result) + public void OnRevertResult(JudgementResult result) { - comboDisplay.OnRevertResult(hitObject, result); - Catcher.OnRevertResult(hitObject, result); + comboDisplay.OnRevertResult(result); + Catcher.OnRevertResult(result); } protected override void Update() diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneObjectPlacement.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneObjectPlacement.cs new file mode 100644 index 0000000000..13a116b209 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneObjectPlacement.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneObjectPlacement : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Test] + public void TestPlacementBeforeTrackStart() + { + AddStep("Seek to 0", () => EditorClock.Seek(0)); + AddStep("Select note", () => InputManager.Key(Key.Number2)); + AddStep("Hover negative span", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First(x => x.Name == "Icons").Children[0]); + }); + AddStep("Click", () => InputManager.Click(MouseButton.Left)); + AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0)); + } + + [Test] + public void TestSeekOnNotePlacement() + { + double? initialTime = null; + + AddStep("store initial time", () => initialTime = EditorClock.CurrentTime); + AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true)); + placeObject(); + AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking); + AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime)); + } + + [Test] + public void TestNoSeekOnNotePlacement() + { + double? initialTime = null; + + AddStep("store initial time", () => initialTime = EditorClock.CurrentTime); + AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false)); + placeObject(); + AddAssert("not seeking", () => !EditorClock.IsSeeking); + AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime)); + } + + private void placeObject() + { + AddStep("select note placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType().Last().ScreenSpaceDrawQuad.Centre)); + AddStep("place note", () => InputManager.Click(MouseButton.Left)); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs deleted file mode 100644 index 00dd75ceee..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Game.Tests.Visual; -using osuTK.Input; - -namespace osu.Game.Rulesets.Mania.Tests.Editor -{ - public partial class TestScenePlacementBeforeTrackStart : EditorTestScene - { - protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); - - [Test] - public void TestPlacement() - { - AddStep("Seek to 0", () => EditorClock.Seek(0)); - AddStep("Select note", () => InputManager.Key(Key.Number2)); - AddStep("Hover negative span", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().First(x => x.Name == "Icons").Children[0]); - }); - AddStep("Click", () => InputManager.Click(MouseButton.Left)); - AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0)); - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs new file mode 100644 index 0000000000..37b31d1d1a --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs @@ -0,0 +1,31 @@ +// 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.Rulesets.Osu.Mods; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModAutopilot : OsuModTestScene + { + [Test] + public void TestInstantResume() + { + CreateModTest(new ModTestData + { + Mod = new OsuModAutopilot(), + PassCondition = () => true, + Autoplay = false, + }); + + AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value); + AddStep("press pause", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait until paused", () => Player.GameplayClockContainer.IsPaused.Value); + AddStep("release pause", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("press resume", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for resume", () => !Player.IsResuming); + AddAssert("resumed", () => !Player.GameplayClockContainer.IsPaused.Value); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs new file mode 100644 index 0000000000..3c32b4fa65 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -0,0 +1,156 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneHitCircleLateFade : OsuTestScene + { + private float? alphaAtMiss; + + [Test] + public void TestHitCircleClassicMod() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = new Mod[] { new OsuModClassic() }; + createCircle(); + }); + + AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Transparent when missed", () => alphaAtMiss == 0); + } + + [Test] + public void TestHitCircleClassicAndFullHiddenMods() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModClassic() }; + createCircle(); + }); + + AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Transparent when missed", () => alphaAtMiss == 0); + } + + [Test] + public void TestHitCircleClassicAndApproachCircleOnlyHiddenMods() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModClassic() }; + createCircle(); + }); + + AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Transparent when missed", () => alphaAtMiss == 0); + } + + [Test] + public void TestHitCircleNoMod() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = Array.Empty(); + createCircle(); + }); + + AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Opaque when missed", () => alphaAtMiss == 1); + } + + [Test] + public void TestSliderClassicMod() + { + AddStep("Create slider", () => + { + SelectedMods.Value = new Mod[] { new OsuModClassic() }; + createSlider(); + }); + + AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Head circle transparent when missed", () => alphaAtMiss == 0); + } + + [Test] + public void TestSliderNoMod() + { + AddStep("Create slider", () => + { + SelectedMods.Value = Array.Empty(); + createSlider(); + }); + + AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull()); + AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1); + } + + private void createCircle() + { + alphaAtMiss = null; + + DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle + { + StartTime = Time.Current + 500, + Position = new Vector2(250) + }); + + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawableHitCircle); + + drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + drawableHitCircle.OnNewResult += (_, _) => + { + alphaAtMiss = drawableHitCircle.Alpha; + }; + + Child = drawableHitCircle; + } + + private void createSlider() + { + alphaAtMiss = null; + + DrawableSlider drawableSlider = new DrawableSlider(new Slider + { + StartTime = Time.Current + 500, + Position = new Vector2(250), + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(0, 100), + }) + }); + + drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + drawableSlider.OnLoadComplete += _ => + { + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle); + + drawableSlider.HeadCircle.OnNewResult += (_, _) => + { + alphaAtMiss = drawableSlider.HeadCircle.Alpha; + }; + }; + Child = drawableSlider; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 6772cfe0be..9eb0a46bfb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Osu.Mods // Generate the replay frames the cursor should follow replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList(); + + drawableRuleset.UseResumeOverlay = false; } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index e021992f86..250d97c537 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods @@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true); + [SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")] + public Bindable FadeHitCircleEarly { get; } = new Bindable(true); + + private bool usingHiddenFading; + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) @@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods if (ClassicNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); + + usingHiddenFading = drawableRuleset.Mods.OfType().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false; } public void ApplyToDrawableHitObject(DrawableHitObject obj) @@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableSliderHead head: head.TrackFollowCircle = !NoSliderHeadMovement.Value; + if (FadeHitCircleEarly.Value && !usingHiddenFading) + applyEarlyFading(head); break; case DrawableSliderTail tail: tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; break; + + case DrawableHitCircle circle: + if (FadeHitCircleEarly.Value && !usingHiddenFading) + applyEarlyFading(circle); + break; } } + + private void applyEarlyFading(DrawableHitCircle circle) + { + circle.ApplyCustomUpdateState += (o, _) => + { + using (o.BeginAbsoluteSequence(o.StateUpdateTime)) + { + double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok); + double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow; + o.Delay(okWindow).FadeOut(lateMissFadeTime); + } + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index b4004ff572..76ae7340ff 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableOsuJudgement : DrawableJudgement { - protected SkinnableLighting Lighting { get; private set; } + internal SkinnableLighting Lighting { get; private set; } [Resolved] private OsuConfigManager config { get; set; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs index 68b61f9b2b..b39b9c4c54 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class SkinnableLighting : SkinnableSprite + internal partial class SkinnableLighting : SkinnableSprite { private DrawableHitObject targetObject; private JudgementResult targetResult; diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 3bab91bd9c..04b8c6dd4e 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -66,15 +66,15 @@ namespace osu.Game.Tests.Skins { var skin = new TestSkin(new SkinInfo(), null, storage); - foreach (var target in skin.DrawableComponentInfo) + foreach (var target in skin.LayoutInfos) { - foreach (var info in target.Value) + foreach (var info in target.Value.AllDrawables) instantiatedTypes.Add(info.Type); } } } - var editableTypes = SkinnableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISkinnableDrawable)?.IsEditable == true); + var editableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true); Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes)); } @@ -87,8 +87,8 @@ namespace osu.Game.Tests.Skins { var skin = new TestSkin(new SkinInfo(), null, storage); - Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2)); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(9)); + Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9)); } } @@ -100,11 +100,11 @@ namespace osu.Game.Tests.Skins { var skin = new TestSkin(new SkinInfo(), null, storage); - Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2)); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(6)); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect], Has.Length.EqualTo(1)); + Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1)); - var skinnableInfo = skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect].First(); + var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First(); Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite))); Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name")); @@ -115,10 +115,10 @@ namespace osu.Game.Tests.Skins using (var storage = new ZipArchiveReader(stream)) { var skin = new TestSkin(new SkinInfo(), null, storage); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(8)); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); - Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 94cb8517e9..514a2d7e84 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -13,13 +13,11 @@ using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -38,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEmptyLegacyBeatmapSkinFallsBack() { CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); - AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); - AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType.MainHUDComponents, skinManager.CurrentSkin.Value)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value)); } protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func getBeatmapSkin) @@ -54,17 +52,17 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected bool AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType target, ISkin expectedSource) + protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource) { - var targetContainer = Player.ChildrenOfType().First(s => s.Target == target); + var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); if (actualComponentsContainer == null) return false; - var actualInfo = actualComponentsContainer.CreateSkinnableInfo(); + var actualInfo = actualComponentsContainer.CreateSerialisedInfo(); - var expectedComponentsContainer = expectedSource.GetDrawableComponent(new GlobalSkinComponentLookup(target)) as Container; + var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container; if (expectedComponentsContainer == null) return false; @@ -85,21 +83,21 @@ namespace osu.Game.Tests.Visual.Gameplay Add(expectedComponentsAdjustmentContainer); expectedComponentsAdjustmentContainer.UpdateSubTree(); - var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo(); + var expectedInfo = expectedComponentsContainer.CreateSerialisedInfo(); Remove(expectedComponentsAdjustmentContainer, true); return almostEqual(actualInfo, expectedInfo); } - private static bool almostEqual(SkinnableInfo info, SkinnableInfo? other) => + private static bool almostEqual(SerialisedDrawableInfo drawableInfo, SerialisedDrawableInfo? other) => other != null - && info.Type == other.Type - && info.Anchor == other.Anchor - && info.Origin == other.Origin - && Precision.AlmostEquals(info.Position, other.Position, 1) - && Precision.AlmostEquals(info.Scale, other.Scale) - && Precision.AlmostEquals(info.Rotation, other.Rotation) - && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); + && drawableInfo.Type == other.Type + && drawableInfo.Anchor == other.Anchor + && drawableInfo.Origin == other.Origin + && Precision.AlmostEquals(drawableInfo.Position, other.Position, 1) + && Precision.AlmostEquals(drawableInfo.Scale, other.Scale) + && Precision.AlmostEquals(drawableInfo.Rotation, other.Rotation) + && drawableInfo.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 5e1412d79b..b918c5e64a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -235,8 +235,8 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); - AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); - AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); AddStep("bind on update", () => { @@ -254,10 +254,10 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); - AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); - AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); - AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded); + AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); + AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded); } private void createNew(Action? action = null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 55ee6c9fc9..0469df1de3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -19,7 +19,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -37,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay private TestDrawablePoolingRuleset drawableRuleset; + private TestPlayfield playfield => (TestPlayfield)drawableRuleset.Playfield; + [Test] public void TestReusedWithHitObjectsSpacedFarApart() { @@ -133,29 +134,49 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("no DHOs shown", () => !this.ChildrenOfType().Any()); } + [Test] + public void TestRevertResult() + { + ManualClock clock = null; + Beatmap beatmap; + + createTest(beatmap = new Beatmap + { + HitObjects = + { + new TestHitObject { StartTime = 0 }, + new TestHitObject { StartTime = 500 }, + new TestHitObject { StartTime = 1000 }, + } + }, 10, () => new FramedClock(clock = new ManualClock())); + + AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100); + AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + + AddStep("rewind to middle", () => clock.CurrentTime = beatmap.HitObjects[1].StartTime - 100); + AddUntilStep("some results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(1)); + + AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100); + AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + + AddStep("disable frame stability", () => drawableRuleset.FrameStablePlayback = false); + AddStep("instant seek to start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime - 100); + AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0)); + } + [Test] public void TestApplyHitResultOnKilled() { ManualClock clock = null; - bool anyJudged = false; - - void onNewResult(JudgementResult _) => anyJudged = true; var beatmap = new Beatmap(); beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 }); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); - AddStep("subscribe to new result", () => - { - anyJudged = false; - drawableRuleset.NewResult += onNewResult; - }); AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000); - AddAssert("object judged", () => anyJudged); - - AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult); + AddAssert("object judged", () => playfield.JudgedObjects.Count == 1); } private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) @@ -212,12 +233,24 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestPlayfield : Playfield { + public readonly HashSet JudgedObjects = new HashSet(); + private readonly int poolSize; public TestPlayfield(int poolSize) { this.poolSize = poolSize; AddInternal(HitObjectContainer); + NewResult += (_, r) => + { + Assert.That(JudgedObjects, Has.No.Member(r.HitObject)); + JudgedObjects.Add(r.HitObject); + }; + RevertResult += r => + { + Assert.That(JudgedObjects, Has.Member(r.HitObject)); + JudgedObjects.Remove(r.HitObject); + }; } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 86d97b4999..62fdb67a30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddStep("reload skin editor", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs index 2f572b46c9..d0e516ed39 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs @@ -22,12 +22,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(ScoreProcessor))] private TestScoreProcessor scoreProcessor = new TestScoreProcessor(); - private readonly OsuHitWindows hitWindows = new OsuHitWindows(); + private readonly OsuHitWindows hitWindows; private UnstableRateCounter counter; private double prev; + public TestSceneUnstableRateCounter() + { + hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(5); + } + [SetUpSteps] public void SetUp() { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 565a919fb8..70ad6bfc96 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -178,6 +178,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorShowHitMarkers, true); + SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true); SetDefault(OsuSetting.LastProcessedMetadataId, -1); @@ -374,6 +375,7 @@ namespace osu.Game.Configuration SeasonalBackgroundMode, EditorWaveformOpacity, EditorShowHitMarkers, + EditorAutoSeekOnPlacement, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, ShowOnlineExplicitContent, diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 375960305c..915a2292a2 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -1,12 +1,7 @@ // 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.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Configuration; -using osu.Game.Screens.Play.HUD; -using osu.Game.Skinning; using osuTK; namespace osu.Game.Extensions @@ -48,42 +43,5 @@ namespace osu.Game.Extensions /// The delta vector in Parent's coordinates. public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); - - public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component); - - public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info) - { - // todo: can probably make this better via deserialisation directly using a common interface. - component.Position = info.Position; - component.Rotation = info.Rotation; - component.Scale = info.Scale; - component.Anchor = info.Anchor; - component.Origin = info.Origin; - - if (component is ISkinnableDrawable skinnable) - { - skinnable.UsesFixedAnchor = info.UsesFixedAnchor; - - foreach (var (_, property) in component.GetSettingsSourceProperties()) - { - var bindable = ((IBindable)property.GetValue(component)!); - - if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue)) - { - // TODO: We probably want to restore default if not included in serialisation information. - // This is not simple to do as SetDefault() is only found in the typed Bindable interface right now. - continue; - } - - skinnable.CopyAdjustedSetting(bindable, settingValue); - } - } - - if (component is Container container) - { - foreach (var child in info.Children) - container.Add(child.CreateInstance()); - } - } } } diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 25830c9d54..6f6292c3b2 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers /// public const float BREAK_LIGHTEN_AMOUNT = 0.3f; - protected const double BACKGROUND_FADE_DURATION = 800; + public const double BACKGROUND_FADE_DURATION = 800; /// /// Whether or not user-configured settings relating to brightness of elements should be ignored diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 96c08aa6f8..f4e23ae7cb 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers"); + /// + /// "Automatically seek after placing objects" + /// + public static LocalisableString AutoSeekOnPlacement => new TranslatableString(getKey(@"auto_seek_on_placement"), @"Automatically seek after placing objects"); + /// /// "Timing" /// diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index e5154fd631..e6d7bcd97d 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -27,7 +28,14 @@ namespace osu.Game.Overlays.Mods public Color4 AccentColour { get => headerBackground.Colour; - set => headerBackground.Colour = value; + set + { + headerBackground.Colour = value; + + var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV(); + var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f); + triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f)); + } } /// @@ -44,6 +52,7 @@ namespace osu.Game.Overlays.Mods private readonly Box headerBackground; private readonly Container contentContainer; private readonly Box contentBackground; + private readonly TrianglesV2 triangles; private const float header_height = 42; @@ -73,6 +82,13 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Height = header_height + ModSelectPanel.CORNER_RADIUS }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.X, + Height = header_height, + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Velocity = 0.7f, + }, headerText = new OsuTextFlowContainer(t => { t.Font = OsuFont.TorusAlternate.With(size: 17); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 5cce0f8c5b..e6662e2179 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Notifications } } - public string CompletionText { get; set; } = "Task has completed!"; + public LocalisableString CompletionText { get; set; } = "Task has completed!"; private float progress; diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index 6f7faf535b..8b15bc8f72 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Utils; using osu.Game.Graphics; @@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input RelativeSizeAxes = Axes.Both, Colour = colour.Gray1, }, - usableAreaContainer = new Container + usableAreaContainer = new UsableAreaContainer(handler) { Origin = Anchor.Centre, Children = new Drawable[] @@ -225,4 +226,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input tabletContainer.Scale = new Vector2(1 / adjust); } } + + public partial class UsableAreaContainer : Container + { + private readonly Bindable areaOffset; + + public UsableAreaContainer(ITabletHandler tabletHandler) + { + areaOffset = tabletHandler.AreaOffset.GetBoundCopy(); + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + var newPos = Position + e.Delta; + this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size)); + } + + protected override void OnDragEnd(DragEndEvent e) + { + areaOffset.Value = Position; + base.OnDragEnd(e); + } + } } diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index 893bc4bac2..034ca11c5c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -5,40 +5,53 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.SkinEditor { - public partial class SkinBlueprint : SelectionBlueprint + public partial class SkinBlueprint : SelectionBlueprint { private Container box = null!; - private Container outlineBox = null!; - private AnchorOriginVisualiser anchorOriginVisualiser = null!; + private OsuSpriteText label = null!; + private Drawable drawable => (Drawable)Item; protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent; - [Resolved] - private OsuColour colours { get; set; } = null!; + private Quad drawableQuad; - public SkinBlueprint(ISkinnableDrawable component) + public override Quad ScreenSpaceDrawQuad => drawableQuad; + public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad; + + public override bool Contains(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos); + + public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition); + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => + drawableQuad.Contains(screenSpacePos); + + public SkinBlueprint(ISerialisableDrawable component) : base(component) { } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { InternalChildren = new Drawable[] { @@ -46,23 +59,26 @@ namespace osu.Game.Overlays.SkinEditor { Children = new Drawable[] { - outlineBox = new Container + new Container { RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = 3, - BorderColour = Color4.White, + CornerRadius = 3, + BorderThickness = SelectionBox.BORDER_RADIUS / 2, + BorderColour = ColourInfo.GradientVertical(colours.Pink4.Darken(0.4f), colours.Pink4), Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Alpha = 0f, + Blending = BlendingParameters.Additive, + Alpha = 0.2f, + Colour = ColourInfo.GradientVertical(colours.Pink2, colours.Pink4), AlwaysPresent = true, }, } }, - new OsuSpriteText + label = new OsuSpriteText { Text = Item.GetType().Name, Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), @@ -86,6 +102,18 @@ namespace osu.Game.Overlays.SkinEditor this.FadeInFromZero(200, Easing.OutQuint); } + protected override bool OnHover(HoverEvent e) + { + updateSelectedState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateSelectedState(); + base.OnHoverLost(e); + } + protected override void OnSelected() { // base logic hides selected blueprints when not selected, but skin blueprints don't do that. @@ -100,73 +128,73 @@ namespace osu.Game.Overlays.SkinEditor private void updateSelectedState() { - outlineBox.FadeColour(colours.Pink.Opacity(IsSelected ? 1 : 0.5f), 200, Easing.OutQuint); - outlineBox.Child.FadeTo(IsSelected ? 0.2f : 0, 200, Easing.OutQuint); - anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint); + label.FadeTo(IsSelected || IsHovered ? 1 : 0, 200, Easing.OutQuint); } - private Quad drawableQuad; - - public override Quad ScreenSpaceDrawQuad => drawableQuad; - protected override void Update() { base.Update(); - drawableQuad = drawable.ScreenSpaceDrawQuad; - var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad); + drawableQuad = drawable.ToScreenSpace( + drawable.DrawRectangle + .Inflate(SkinSelectionHandler.INFLATE_SIZE)); - box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this); - box.Size = quad.Size; + var localSpaceQuad = ToLocalSpace(drawableQuad); + + box.Position = localSpaceQuad.TopLeft; + box.Size = localSpaceQuad.Size; box.Rotation = drawable.Rotation; box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y)); } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos); - - public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition); - - public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad; } internal partial class AnchorOriginVisualiser : CompositeDrawable { private readonly Drawable drawable; - private readonly Box originBox; + private Drawable originBox = null!; - private readonly Box anchorBox; - private readonly Box anchorLine; + private Drawable anchorBox = null!; + private Drawable anchorLine = null!; public AnchorOriginVisualiser(Drawable drawable) { this.drawable = drawable; + } - InternalChildren = new Drawable[] + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Color4 anchorColour = colours.Red1; + Color4 originColour = colours.Red3; + + InternalChildren = new[] { - anchorLine = new Box + anchorLine = new Circle { - Height = 2, + Height = 3f, Origin = Anchor.CentreLeft, - Colour = Color4.Yellow, - EdgeSmoothness = Vector2.One + Colour = ColourInfo.GradientHorizontal(originColour.Opacity(0.5f), originColour), }, - originBox = new Box + originBox = new Circle { - Colour = Color4.Red, + Colour = originColour, Origin = Anchor.Centre, - Size = new Vector2(5), + Size = new Vector2(7), }, - anchorBox = new Box + anchorBox = new Circle { - Colour = Color4.Red, + Colour = anchorColour, Origin = Anchor.Centre, - Size = new Vector2(5), + Size = new Vector2(10), }, }; } + private Vector2? anchorPosition; + private Vector2? originPositionInDrawableSpace; + protected override void Update() { base.Update(); @@ -174,8 +202,13 @@ namespace osu.Game.Overlays.SkinEditor if (drawable.Parent == null) return; - originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this); - anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this); + var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this); + anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor); + anchorBox.Position = anchorPosition.Value; + + // for the origin, tween in the drawable's local space to avoid unwanted tweening when the drawable is being dragged. + originPositionInDrawableSpace = originPositionInDrawableSpace != null ? tweenPosition(originPositionInDrawableSpace.Value, drawable.OriginPosition) : drawable.OriginPosition; + originBox.Position = drawable.ToSpaceOfOtherDrawable(originPositionInDrawableSpace.Value, this); var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre); var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre); @@ -184,5 +217,11 @@ namespace osu.Game.Overlays.SkinEditor anchorLine.Width = (point2 - point1).Length; anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X)); } + + private Vector2 tweenPosition(Vector2 oldPosition, Vector2 newPosition) + => new Vector2( + (float)Interpolation.DampContinuously(oldPosition.X, newPosition.X, 25, Clock.ElapsedFrameTime), + (float)Interpolation.DampContinuously(oldPosition.Y, newPosition.Y, 25, Clock.ElapsedFrameTime) + ); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index a448b3d0c1..08b4c85e4a 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -24,11 +24,11 @@ using osuTK.Input; namespace osu.Game.Overlays.SkinEditor { - public partial class SkinBlueprintContainer : BlueprintContainer + public partial class SkinBlueprintContainer : BlueprintContainer { private readonly Drawable target; - private readonly List> targetComponents = new List>(); + private readonly List> targetComponents = new List>(); [Resolved] private SkinEditor editor { get; set; } = null!; @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.BindTo(editor.SelectedComponents); // track each target container on the current screen. - var targetContainers = target.ChildrenOfType().ToArray(); + var targetContainers = target.ChildrenOfType().ToArray(); if (targetContainers.Length == 0) { @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.SkinEditor foreach (var targetContainer in targetContainers) { - var bindableList = new BindableList { BindTarget = targetContainer.Components }; + var bindableList = new BindableList { BindTarget = targetContainer.Components }; bindableList.BindCollectionChanged(componentsChanged, true); targetComponents.Add(bindableList); @@ -69,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); - foreach (var item in e.NewItems.Cast()) + foreach (var item in e.NewItems.Cast()) AddBlueprintFor(item); break; @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.SkinEditor case NotifyCollectionChangedAction.Reset: Debug.Assert(e.OldItems != null); - foreach (var item in e.OldItems.Cast()) + foreach (var item in e.OldItems.Cast()) RemoveBlueprintFor(item); break; @@ -85,16 +85,16 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(e.NewItems != null); Debug.Assert(e.OldItems != null); - foreach (var item in e.OldItems.Cast()) + foreach (var item in e.OldItems.Cast()) RemoveBlueprintFor(item); - foreach (var item in e.NewItems.Cast()) + foreach (var item in e.NewItems.Cast()) AddBlueprintFor(item); break; } }); - protected override void AddBlueprintFor(ISkinnableDrawable item) + protected override void AddBlueprintFor(ISerialisableDrawable item) { if (!item.IsEditable) return; @@ -145,12 +145,12 @@ namespace osu.Game.Overlays.SkinEditor // convert to game space coordinates delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); - SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); + SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); } - protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); + protected override SelectionHandler CreateSelectionHandler() => new SkinSelectionHandler(); - protected override SelectionBlueprint CreateBlueprintFor(ISkinnableDrawable component) + protected override SelectionBlueprint CreateBlueprintFor(ISerialisableDrawable component) => new SkinBlueprint(component); protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index 28ceaf09fc..d0707abec6 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -2,17 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Logging; -using osu.Game.Graphics; +using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Screens.Edit.Components; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor { fill.Clear(); - var skinnableTypes = SkinnableInfo.GetAllAvailableDrawables(); + var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(); foreach (var type in skinnableTypes) attemptAddComponent(type); } @@ -61,11 +61,12 @@ namespace osu.Game.Overlays.SkinEditor { Drawable instance = (Drawable)Activator.CreateInstance(type)!; - if (!((ISkinnableDrawable)instance).IsEditable) return; + if (!((ISerialisableDrawable)instance).IsEditable) return; fill.Add(new ToolboxComponentButton(instance, target) { - RequestPlacement = t => RequestPlacement?.Invoke(t) + RequestPlacement = t => RequestPlacement?.Invoke(t), + Expanding = contractOtherButtons, }); } catch (DependencyNotRegisteredException) @@ -79,15 +80,29 @@ namespace osu.Game.Overlays.SkinEditor } } + private void contractOtherButtons(ToolboxComponentButton obj) + { + foreach (var b in fill.OfType()) + { + if (b == obj) + continue; + + b.Contract(); + } + } + public partial class ToolboxComponentButton : OsuButton { public Action? RequestPlacement; + public Action? Expanding; private readonly Drawable component; private readonly CompositeDrawable? dependencySource; private Container innerContainer = null!; + private ScheduledDelegate? expandContractAction; + private const float contracted_size = 60; private const float expanded_size = 120; @@ -102,20 +117,45 @@ namespace osu.Game.Overlays.SkinEditor Height = contracted_size; } + private const double animation_duration = 500; + protected override bool OnHover(HoverEvent e) { - this.Delay(300).ResizeHeightTo(expanded_size, 500, Easing.OutQuint); + expandContractAction?.Cancel(); + expandContractAction = Scheduler.AddDelayed(() => + { + this.ResizeHeightTo(expanded_size, animation_duration, Easing.OutQuint); + Expanding?.Invoke(this); + }, 100); + return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - this.ResizeHeightTo(contracted_size, 500, Easing.OutQuint); + + expandContractAction?.Cancel(); + // If no other component is selected for too long, force a contract. + // Otherwise we will generally contract when Contract() is called from outside. + expandContractAction = Scheduler.AddDelayed(Contract, 1000); + } + + public void Contract() + { + // Cheap debouncing to avoid stacking animations. + // The only place this is nulled is at the end of this method. + if (expandContractAction == null) + return; + + this.ResizeHeightTo(contracted_size, animation_duration, Easing.OutQuint); + + expandContractAction?.Cancel(); + expandContractAction = null; } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider) { BackgroundColour = colourProvider.Background3; diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 866de7e621..133ec10202 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.SkinEditor public const float MENU_HEIGHT = 40; - public readonly BindableList SelectedComponents = new BindableList(); + public readonly BindableList SelectedComponents = new BindableList(); protected override bool StartHidden => true; @@ -302,13 +302,13 @@ namespace osu.Game.Overlays.SkinEditor private void placeComponent(Type type) { - if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) - throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}."); + if (!(Activator.CreateInstance(type) is ISerialisableDrawable component)) + throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}."); placeComponent(component); } - private void placeComponent(ISkinnableDrawable component, bool applyDefaults = true) + private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true) { var targetContainer = getFirstTarget(); @@ -339,25 +339,25 @@ namespace osu.Game.Overlays.SkinEditor settingsSidebar.Add(new SkinSettingsToolbox(component)); } - private IEnumerable availableTargets => targetScreen.ChildrenOfType(); + private IEnumerable availableTargets => targetScreen.ChildrenOfType(); - private ISkinnableTarget? getFirstTarget() => availableTargets.FirstOrDefault(); + private ISerialisableDrawableContainer? getFirstTarget() => availableTargets.FirstOrDefault(); - private ISkinnableTarget? getTarget(GlobalSkinComponentLookup.LookupType target) + private ISerialisableDrawableContainer? getTarget(SkinComponentsContainerLookup.TargetArea target) { - return availableTargets.FirstOrDefault(c => c.Target == target); + return availableTargets.FirstOrDefault(c => c.Lookup.Target == target); } private void revert() { - ISkinnableTarget[] targetContainers = availableTargets.ToArray(); + SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); foreach (var t in targetContainers) { currentSkin.Value.ResetDrawableTarget(t); // add back default components - getTarget(t.Target)?.Reload(); + getTarget(t.Lookup.Target)?.Reload(); } } @@ -370,7 +370,7 @@ namespace osu.Game.Overlays.SkinEditor if (!hasBegunMutating) return; - ISkinnableTarget[] targetContainers = availableTargets.ToArray(); + SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); foreach (var t in targetContainers) currentSkin.Value.UpdateDrawableTarget(t); @@ -400,7 +400,7 @@ namespace osu.Game.Overlays.SkinEditor this.FadeOut(TRANSITION_DURATION, Easing.OutQuint); } - public void DeleteItems(ISkinnableDrawable[] items) + public void DeleteItems(ISerialisableDrawable[] items) { foreach (var item in items) availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs index c8f66f3e56..d1a1850796 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs @@ -9,19 +9,17 @@ using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Extensions; using osu.Game.Screens.Edit; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; namespace osu.Game.Overlays.SkinEditor { public partial class SkinEditorChangeHandler : EditorChangeHandler { - private readonly ISkinnableTarget? firstTarget; + private readonly ISerialisableDrawableContainer? firstTarget; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable - private readonly BindableList? components; + private readonly BindableList? components; public SkinEditorChangeHandler(Drawable targetScreen) { @@ -29,12 +27,12 @@ namespace osu.Game.Overlays.SkinEditor // In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`. // We'll also need to consider cases where multiple targets are on screen at the same time. - firstTarget = targetScreen.ChildrenOfType().FirstOrDefault(); + firstTarget = targetScreen.ChildrenOfType().FirstOrDefault(); if (firstTarget == null) return; - components = new BindableList { BindTarget = firstTarget.Components }; + components = new BindableList { BindTarget = firstTarget.Components }; components.BindCollectionChanged((_, _) => SaveState()); } @@ -43,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor if (firstTarget == null) return; - var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray(); + var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray(); string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented }); stream.Write(Encoding.UTF8.GetBytes(json)); } @@ -53,12 +51,12 @@ namespace osu.Game.Overlays.SkinEditor if (firstTarget == null) return; - var deserializedContent = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(newState)); + var deserializedContent = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(newState)); if (deserializedContent == null) return; - SkinnableInfo[] skinnableInfo = deserializedContent.ToArray(); + SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray(); Drawable[] targetComponents = firstTarget.Components.OfType().ToArray(); if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType()))) @@ -71,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor int i = 0; foreach (var drawable in targetComponents) - drawable.ApplySkinnableInfo(skinnableInfo[i++]); + drawable.ApplySerialisedInfo(skinnableInfo[i++]); } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 86fcd35e03..c628ad8480 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Overlays.SkinEditor { - public partial class SkinSelectionHandler : SelectionHandler + public partial class SkinSelectionHandler : SelectionHandler { [Resolved] private SkinEditor skinEditor { get; set; } = null!; @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.SkinEditor return true; } - public override bool HandleMovement(MoveSelectionEvent moveEvent) + public override bool HandleMovement(MoveSelectionEvent moveEvent) { foreach (var c in SelectedBlueprints) { @@ -178,10 +178,10 @@ namespace osu.Game.Overlays.SkinEditor SelectionBox.CanReverse = false; } - protected override void DeleteItems(IEnumerable items) => + protected override void DeleteItems(IEnumerable items) => skinEditor.DeleteItems(items.ToArray()); - protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { @@ -209,7 +209,7 @@ namespace osu.Game.Overlays.SkinEditor foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) + IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) { var displayableAnchors = new[] { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b5b7400f64..aee86fd942 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -17,6 +17,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; @@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Edit private FillFlowContainer togglesCollection; private IBindable hasTiming; + private Bindable autoSeekOnPlacement; protected HitObjectComposer(Ruleset ruleset) : base(ruleset) @@ -80,8 +82,10 @@ namespace osu.Game.Rulesets.Edit dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuConfigManager config) { + autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); + Config = Dependencies.Get().GetConfigFor(Ruleset); try @@ -365,7 +369,7 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.Add(hitObject); - if (EditorClock.CurrentTime < hitObject.StartTime) + if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) EditorClock.SeekSmoothlyTo(hitObject.StartTime); } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 2685cb4e2a..bf29919e34 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using JetBrains.Annotations; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -33,16 +34,30 @@ namespace osu.Game.Rulesets.Judgements public readonly Judgement Judgement; /// - /// The offset from a perfect hit at which this occurred. + /// The time at which this occurred. /// Populated when this is applied via . /// - public double TimeOffset { get; internal set; } + /// + /// This is used instead of to check whether this should be reverted. + /// + internal double? RawTime { get; set; } /// - /// The absolute time at which this occurred. - /// Equal to the (end) time of the + . + /// The offset of from the end time of , clamped by . /// - public double TimeAbsolute => HitObject.GetEndTime() + TimeOffset; + public double TimeOffset + { + get => RawTime != null ? Math.Min(RawTime.Value - HitObject.GetEndTime(), HitObject.MaximumJudgementOffset) : 0; + internal set => RawTime = HitObject.GetEndTime() + value; + } + + /// + /// The absolute time at which this occurred, clamped by the end time of plus . + /// + /// + /// The end time of is returned if this result is not populated yet. + /// + public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime(); /// /// The combo prior to this occurring. @@ -83,6 +98,13 @@ namespace osu.Game.Rulesets.Judgements { HitObject = hitObject; Judgement = judgement; + Reset(); + } + + internal void Reset() + { + Type = HitResult.None; + RawTime = null; } public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})"; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 40b51ddcca..0c50f8341a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked by this or a nested prior to a being reverted. /// + /// + /// This is only invoked if this is alive when the result is reverted. + /// public event Action OnRevertResult; /// @@ -222,6 +225,8 @@ namespace osu.Game.Rulesets.Objects.Drawables ensureEntryHasResult(); + entry.RevertResult += onRevertResult; + foreach (var h in HitObject.NestedHitObjects) { var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this); @@ -234,7 +239,6 @@ namespace osu.Game.Rulesets.Objects.Drawables OnNestedDrawableCreated?.Invoke(drawableNested); drawableNested.OnNewResult += onNewResult; - drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation(). @@ -308,7 +312,6 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var obj in nestedHitObjects) { obj.OnNewResult -= onNewResult; - obj.OnRevertResult -= onRevertResult; obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; } @@ -317,6 +320,8 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied -= onDefaultsApplied; + entry.RevertResult -= onRevertResult; + OnFree(); ParentHitObject = null; @@ -365,7 +370,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); - private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); + private void onRevertResult() + { + updateState(ArmedState.Idle); + OnRevertResult?.Invoke(this, Result); + } private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state); @@ -577,26 +586,6 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion - protected override void Update() - { - base.Update(); - - if (Result != null && Result.HasResult) - { - double endTime = HitObject.GetEndTime(); - - if (Result.TimeOffset + endTime > Time.Current) - { - OnRevertResult?.Invoke(this, Result); - - Result.TimeOffset = 0; - Result.Type = HitResult.None; - - updateState(ArmedState.Idle); - } - } - } - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; protected override void UpdateAfterChildren() @@ -671,7 +660,7 @@ namespace osu.Game.Rulesets.Objects.Drawables $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); } - Result.TimeOffset = Math.Min(HitObject.MaximumJudgementOffset, Time.Current - HitObject.GetEndTime()); + Result.RawTime = Time.Current; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index fedf419973..b517f6b9e6 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -1,6 +1,7 @@ // 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 osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Objects private readonly IBindable startTimeBindable = new BindableDouble(); + internal event Action? RevertResult; + /// /// Creates a new . /// @@ -95,5 +98,7 @@ namespace osu.Game.Rulesets.Objects /// Set using . /// internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + + internal void OnRevertResult() => RevertResult?.Invoke(); } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 71b452c309..4f4640872f 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.UI playfield = new Lazy(() => CreatePlayfield().With(p => { p.NewResult += (_, r) => NewResult?.Invoke(r); - p.RevertResult += (_, r) => RevertResult?.Invoke(r); + p.RevertResult += r => RevertResult?.Invoke(r); })); } @@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.UI public override void RequestResume(Action continueResume) { - if (ResumeOverlay != null && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)))) + if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)))) { ResumeOverlay.GameplayCursor = Cursor; ResumeOverlay.ResumeAction = continueResume; @@ -507,6 +507,15 @@ namespace osu.Game.Rulesets.UI /// public ResumeOverlay ResumeOverlay { get; protected set; } + /// + /// Whether the should be used to return the user's cursor position to its previous location after a pause. + /// + /// + /// Defaults to true. + /// Even if true, will not have any effect if the ruleset does not have a resume overlay (see ). + /// + public bool UseResumeOverlay { get; set; } = true; + /// /// Returns first available provided by a . /// @@ -531,6 +540,11 @@ namespace osu.Game.Rulesets.UI } } + /// + /// Create an optional resume overlay, which is displayed when a player requests to resume gameplay during non-break time. + /// This can be used to force the player to return their hands / cursor to the position they left off, to avoid players + /// using pauses as a means of adjusting their inputs (aka "pause buffering"). + /// protected virtual ResumeOverlay CreateResumeOverlay() => null; /// diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 7cbf49aa31..099be486b3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.UI /// public event Action NewResult; - /// - /// Invoked when a judgement is reverted. - /// - public event Action RevertResult; - /// /// Invoked when a becomes used by a . /// @@ -111,7 +106,6 @@ namespace osu.Game.Rulesets.UI private void addDrawable(DrawableHitObject drawable) { drawable.OnNewResult += onNewResult; - drawable.OnRevertResult += onRevertResult; bindStartTime(drawable); AddInternal(drawable); @@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.UI private void removeDrawable(DrawableHitObject drawable) { drawable.OnNewResult -= onNewResult; - drawable.OnRevertResult -= onRevertResult; unbindStartTime(drawable); @@ -154,7 +147,6 @@ namespace osu.Game.Rulesets.UI #endregion private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); - private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); #region Comparator + StartTime tracking diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a7881678f1..b1c3b78e67 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Objects.Pooling; +using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.UI { @@ -35,9 +36,9 @@ namespace osu.Game.Rulesets.UI public event Action NewResult; /// - /// Invoked when a judgement is reverted. + /// Invoked when a judgement result is reverted. /// - public event Action RevertResult; + public event Action RevertResult; /// /// The contained in this Playfield. @@ -98,6 +99,8 @@ namespace osu.Game.Rulesets.UI private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); + private readonly Stack judgedEntries; + /// /// Creates a new . /// @@ -107,14 +110,15 @@ namespace osu.Game.Rulesets.UI hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => { - h.NewResult += (d, r) => NewResult?.Invoke(d, r); - h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.NewResult += onNewResult; h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryRemoved += onEntryRemoved; + + judgedEntries = new Stack(); } [BackgroundDependencyLoader] @@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); - otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.RevertResult += r => RevertResult?.Invoke(r); otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); @@ -252,6 +256,18 @@ namespace osu.Game.Rulesets.UI updatable.Update(this); } } + + // When rewinding, revert future judgements in the reverse order. + while (judgedEntries.Count > 0) + { + var result = judgedEntries.Peek().Result; + Debug.Assert(result?.RawTime != null); + + if (Time.Current >= result.RawTime.Value) + break; + + revertResult(judgedEntries.Pop()); + } } /// @@ -443,6 +459,25 @@ namespace osu.Game.Rulesets.UI #endregion + private void onNewResult(DrawableHitObject drawable, JudgementResult result) + { + Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); + judgedEntries.Push(drawable.Entry.AsNonNull()); + + NewResult?.Invoke(drawable, result); + } + + private void revertResult(HitObjectLifetimeEntry entry) + { + var result = entry.Result; + Debug.Assert(result != null); + + RevertResult?.Invoke(result); + entry.OnRevertResult(); + + result.Reset(); + } + #region Editor logic /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index a0ac99fec2..9e4fb26688 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public abstract partial class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler, IHasContextMenu { + /// + /// How much padding around the selection area is added. + /// + public const float INFLATE_SIZE = 5; + /// /// The currently selected blueprints. /// Should be used when operations are dealing directly with the visible blueprints. @@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 1; i < selectedBlueprints.Count; i++) selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat); - selectionRect = selectionRect.Inflate(5f); + selectionRect = selectionRect.Inflate(INFLATE_SIZE); SelectionBox.Position = selectionRect.Location; SelectionBox.Size = selectionRect.Size; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bd133383d1..d89392f757 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit private Bindable editorBackgroundDim; private Bindable editorHitMarkers; + private Bindable editorAutoSeekOnPlacement; public Editor(EditorLoader loader = null) { @@ -272,6 +273,7 @@ namespace osu.Game.Screens.Edit editorBackgroundDim = config.GetBindable(OsuSetting.EditorDim); editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers); + editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); AddInternal(new OsuContextMenuContainer { @@ -329,6 +331,10 @@ namespace osu.Game.Screens.Edit new ToggleMenuItem(EditorStrings.ShowHitMarkers) { State = { BindTarget = editorHitMarkers }, + }, + new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) + { + State = { BindTarget = editorAutoSeekOnPlacement }, } } }, diff --git a/osu.Game/Screens/Play/HUD/BPMCounter.cs b/osu.Game/Screens/Play/HUD/BPMCounter.cs index 500ab0169f..cd24237493 100644 --- a/osu.Game/Screens/Play/HUD/BPMCounter.cs +++ b/osu.Game/Screens/Play/HUD/BPMCounter.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { - public partial class BPMCounter : RollingCounter, ISkinnableDrawable + public partial class BPMCounter : RollingCounter, ISerialisableDrawable { protected override double RollingDuration => 750; diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index cb72bb5f6f..1aa7c5e091 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCounter : RollingCounter, ISkinnableDrawable + public partial class ClicksPerSecondCounter : RollingCounter, ISerialisableDrawable { [Resolved] private ClicksPerSecondCalculator calculator { get; set; } = null!; diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs index afccbc4ef0..17531281aa 100644 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ComboCounter.cs @@ -7,7 +7,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public abstract partial class ComboCounter : RollingCounter, ISkinnableDrawable + public abstract partial class ComboCounter : RollingCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 1a082e58b7..eb3c71afbb 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable + public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 62d66efb33..2c43905a46 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -19,7 +19,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable + public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISerialisableDrawable { /// /// The base opacity of the glow. diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index f116617271..7cc2dc1751 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -10,7 +10,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public partial class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable + public partial class DefaultScoreCounter : GameplayScoreCounter, ISerialisableDrawable { public DefaultScoreCounter() { diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index 191f63e97d..5d65208afe 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { - public abstract partial class HitErrorMeter : CompositeDrawable, ISkinnableDrawable + public abstract partial class HitErrorMeter : CompositeDrawable, ISerialisableDrawable { protected HitWindows HitWindows { get; private set; } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 80d2e0863f..a9b59a02b5 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD.JudgementCounter { - public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable + public partial class JudgementCounterDisplay : CompositeDrawable, ISerialisableDrawable { public const int TRANSFORM_DURATION = 250; diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 15484f2965..4f37c215e9 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -35,7 +35,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { - public partial class PerformancePointsCounter : RollingCounter, ISkinnableDrawable + public partial class PerformancePointsCounter : RollingCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 4647c0352b..ebe2fb83e6 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -14,7 +14,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { - public abstract partial class SongProgress : OverlayContainer, ISkinnableDrawable + public abstract partial class SongProgress : OverlayContainer, ISerialisableDrawable { // Some implementations of this element allow seeking during gameplay playback. // Set a sane default of never handling input to override the behaviour provided by OverlayContainer. diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index f450ae799e..4ceca817e2 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -20,7 +20,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { - public partial class UnstableRateCounter : RollingCounter, ISkinnableDrawable + public partial class UnstableRateCounter : RollingCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 4d1f0b96b6..c8d06b82e8 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -26,6 +27,7 @@ using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; using osu.Game.Localisation; +using osu.Game.Rulesets; namespace osu.Game.Screens.Play { @@ -88,7 +90,7 @@ namespace osu.Game.Screens.Play private readonly BindableBool holdingForHUD = new BindableBool(); - private readonly SkinnableTargetContainer mainComponents; + private readonly SkinComponentsContainer mainComponents; /// /// A flow which sits at the left side of the screen to house leaderboard (and related) components. @@ -100,20 +102,22 @@ namespace osu.Game.Screens.Play public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { + Drawable rulesetComponents; + this.drawableRuleset = drawableRuleset; this.mods = mods; RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + Children = new[] { CreateFailingLayer(), //Needs to be initialized before skinnable drawables. tally = new JudgementTally(), - mainComponents = new MainComponentsContainer - { - AlwaysPresent = true, - }, + mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, + rulesetComponents = drawableRuleset != null + ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } + : Empty(), topRightElements = new FillFlowContainer { Anchor = Anchor.TopRight, @@ -155,7 +159,7 @@ namespace osu.Game.Screens.Play clicksPerSecondCalculator = new ClicksPerSecondCalculator(), }; - hideTargets = new List { mainComponents, KeyCounter, topRightElements }; + hideTargets = new List { mainComponents, rulesetComponents, KeyCounter, topRightElements }; if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -390,15 +394,15 @@ namespace osu.Game.Screens.Play } } - private partial class MainComponentsContainer : SkinnableTargetContainer + private partial class HUDComponentsContainer : SkinComponentsContainer { private Bindable scoringMode; [Resolved] private OsuConfigManager config { get; set; } - public MainComponentsContainer() - : base(GlobalSkinComponentLookup.LookupType.MainHUDComponents) + public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null) + : base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, ruleset)) { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index d9062da8aa..4f7e4add32 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -417,7 +417,7 @@ namespace osu.Game.Screens.Play lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) - ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); + ApplyToBackground(b => b.FadeColour(Color4.White, 800, Easing.OutQuint)); } protected virtual void ContentOut() diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs index d6c8a0ad6a..a5d1961bd8 100644 --- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using System.Diagnostics; +using osu.Framework.Screens; using osu.Game.Screens.Backgrounds; namespace osu.Game.Screens.Play @@ -12,6 +12,11 @@ namespace osu.Game.Screens.Play { protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - public void ApplyToBackground(Action action) => base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b)); + public void ApplyToBackground(Action action) + { + Debug.Assert(this.IsCurrentScreen()); + + base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b)); + } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 8786821c77..661eec8e97 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -31,6 +31,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -148,7 +149,7 @@ namespace osu.Game.Screens.Select if (!this.IsCurrentScreen()) return; - ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0); + ApplyToBackground(applyBlurToBackground); }); LoadComponentAsync(Carousel = new BeatmapCarousel @@ -194,6 +195,7 @@ namespace osu.Game.Screens.Select { ParallaxAmount = 0.005f, RelativeSizeAxes = Axes.Both, + Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, Child = new WedgeBackground @@ -273,7 +275,7 @@ namespace osu.Game.Screens.Select } } }, - new SkinnableTargetContainer(GlobalSkinComponentLookup.LookupType.SongSelect) + new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)) { RelativeSizeAxes = Axes.Both, }, @@ -763,12 +765,18 @@ namespace osu.Game.Screens.Select /// The working beatmap. private void updateComponentFromBeatmap(WorkingBeatmap beatmap) { - ApplyToBackground(backgroundModeBeatmap => + // If not the current screen, this will be applied in OnResuming. + if (this.IsCurrentScreen()) { - backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f; - backgroundModeBeatmap.FadeColour(Color4.White, 250); - }); + ApplyToBackground(backgroundModeBeatmap => + { + backgroundModeBeatmap.Beatmap = beatmap; + backgroundModeBeatmap.IgnoreUserSettings.Value = true; + backgroundModeBeatmap.FadeColour(Color4.White, 250); + + applyBlurToBackground(backgroundModeBeatmap); + }); + } beatmapInfoWedge.Beatmap = beatmap; @@ -785,6 +793,14 @@ namespace osu.Game.Screens.Select } } + private void applyBlurToBackground(BackgroundScreenBeatmap backgroundModeBeatmap) + { + backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f; + backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = configBackgroundBlur.Value ? 0 : 0.4f; + + wedgeBackground.FadeTo(configBackgroundBlur.Value ? 0.5f : 0.2f, UserDimContainer.BACKGROUND_FADE_DURATION, Easing.OutQuint); + } + private readonly WeakReference lastTrack = new WeakReference(null); /// diff --git a/osu.Game/Screens/Select/WedgeBackground.cs b/osu.Game/Screens/Select/WedgeBackground.cs index da12c1a67a..2e2b43cd70 100644 --- a/osu.Game/Screens/Select/WedgeBackground.cs +++ b/osu.Game/Screens/Select/WedgeBackground.cs @@ -1,14 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Select { @@ -22,7 +19,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, Size = new Vector2(1, 0.5f), - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black, Shear = new Vector2(0.15f, 0), EdgeSmoothness = new Vector2(2, 0), }, @@ -32,7 +29,7 @@ namespace osu.Game.Screens.Select RelativePositionAxes = Axes.Y, Size = new Vector2(1, -0.5f), Position = new Vector2(0, 1), - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black, Shear = new Vector2(-0.15f, 0), EdgeSmoothness = new Vector2(2, 0), }, diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 71f0888c6f..a9b26f13e8 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -90,10 +90,14 @@ namespace osu.Game.Skinning switch (lookup) { - case GlobalSkinComponentLookup globalLookup: - switch (globalLookup.Lookup) + case SkinComponentsContainerLookup containerLookup: + // Only handle global level defaults for now. + if (containerLookup.Ruleset != null) + return null; + + switch (containerLookup.Target) { - case GlobalSkinComponentLookup.LookupType.SongSelect: + case SkinComponentsContainerLookup.TargetArea.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -101,7 +105,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case GlobalSkinComponentLookup.LookupType.MainHUDComponents: + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Skinning/Components/BigBlackBox.cs b/osu.Game/Skinning/Components/BigBlackBox.cs index 043a276e49..3c63dae8d8 100644 --- a/osu.Game/Skinning/Components/BigBlackBox.cs +++ b/osu.Game/Skinning/Components/BigBlackBox.cs @@ -19,7 +19,7 @@ namespace osu.Game.Skinning.Components /// Intended to be a test bed for skinning. May be removed at some point in the future. /// [UsedImplicitly] - public partial class BigBlackBox : CompositeDrawable, ISkinnableDrawable + public partial class BigBlackBox : CompositeDrawable, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index 2e41b35abb..8f3a1d41c6 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning /// /// A skin component that contains text and allows the user to choose its font. /// - public abstract partial class FontAdjustableSkinComponent : Container, ISkinnableDrawable + public abstract partial class FontAdjustableSkinComponent : Container, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs index 9247ca0e4d..a44bf3a43d 100644 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs @@ -1,12 +1,23 @@ // 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.Linq; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Skinning { + /// + /// A lookup type intended for use for skinnable gameplay components (not HUD level components). + /// + /// + /// The most common usage of this class is for ruleset-specific skinning implementations, but it can also be used directly + /// (see 's usage for ) where ruleset-agnostic elements are required. + /// + /// An enum lookup type. public class GameplaySkinComponentLookup : ISkinComponentLookup - where T : notnull + where T : Enum { public readonly T Component; @@ -16,7 +27,7 @@ namespace osu.Game.Skinning } protected virtual string RulesetPrefix => string.Empty; - protected virtual string ComponentName => Component.ToString() ?? string.Empty; + protected virtual string ComponentName => Component.ToString(); public string LookupName => string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); diff --git a/osu.Game/Skinning/GlobalSkinComponentLookup.cs b/osu.Game/Skinning/GlobalSkinComponentLookup.cs deleted file mode 100644 index 7dcc3c4f14..0000000000 --- a/osu.Game/Skinning/GlobalSkinComponentLookup.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 GlobalSkinComponentLookup : ISkinComponentLookup - { - public readonly LookupType Lookup; - - public GlobalSkinComponentLookup(LookupType lookup) - { - Lookup = lookup; - } - - public enum LookupType - { - MainHUDComponents, - SongSelect - } - } -} diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISerialisableDrawable.cs similarity index 64% rename from osu.Game/Skinning/ISkinnableDrawable.cs rename to osu.Game/Skinning/ISerialisableDrawable.cs index 1ecd6f967e..503b44c2dd 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISerialisableDrawable.cs @@ -10,13 +10,16 @@ using osu.Game.Configuration; namespace osu.Game.Skinning { /// - /// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications. + /// A drawable which is intended to be serialised to . /// /// - /// Attaching this interface to any will make it serialisable to skin settings. - /// Adding annotated bindables will also serialise these settings alongside each instance. + /// This is currently used exclusively for serialisation to a skin, and leaned on heavily to allow placement and customisation in the skin layout editor. + /// That said, it is intended to be flexible enough to potentially be used in other places we want to serialise drawables in the future. + /// + /// Attaching this interface to any will make it serialisable via . + /// Adding annotated bindables will also allow serialising settings automatically. /// - public interface ISkinnableDrawable : IDrawable + public interface ISerialisableDrawable : IDrawable { /// /// Whether this component should be editable by an end user. @@ -24,8 +27,8 @@ namespace osu.Game.Skinning bool IsEditable => true; /// - /// In the context of the skin layout editor, whether this has a permanent anchor defined. - /// If , this 's is automatically determined by proximity, + /// In the context of the skin layout editor, whether this has a permanent anchor defined. + /// If , this 's is automatically determined by proximity, /// If , a fixed anchor point has been defined. /// bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISerialisableDrawableContainer.cs similarity index 61% rename from osu.Game/Skinning/ISkinnableTarget.cs rename to osu.Game/Skinning/ISerialisableDrawableContainer.cs index 3f116f8f76..9f93d8a2e3 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISerialisableDrawableContainer.cs @@ -5,31 +5,25 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Extensions; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { /// - /// Denotes a container which can house s. + /// A container which can house s. + /// Contains functionality for new drawables to be added, removed, and reloaded from provided . /// - public interface ISkinnableTarget : IDrawable + public interface ISerialisableDrawableContainer : IDrawable { - /// - /// The definition of this target. - /// - GlobalSkinComponentLookup.LookupType Target { get; } - /// /// A bindable list of components which are being tracked by this skinnable target. /// - IBindableList Components { get; } + IBindableList Components { get; } /// - /// Serialise all children as . + /// Serialise all children as . /// /// The serialised content. - IEnumerable CreateSkinnableInfo() => Components.Select(d => ((Drawable)d).CreateSkinnableInfo()); + IEnumerable CreateSerialisedInfo() => Components.Select(d => ((Drawable)d).CreateSerialisedInfo()); /// /// Reload this target from the current skin. @@ -39,18 +33,18 @@ namespace osu.Game.Skinning /// /// Reload this target from the provided skinnable information. /// - void Reload(SkinnableInfo[] skinnableInfo); + void Reload(SerialisedDrawableInfo[] skinnableInfo); /// /// Add a new skinnable component to this target. /// /// The component to add. - void Add(ISkinnableDrawable drawable); + void Add(ISerialisableDrawable drawable); /// /// Remove an existing skinnable component from this target. /// /// The component to remove. - void Remove(ISkinnableDrawable component); + void Remove(ISerialisableDrawable component); } } diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 45be5582f6..fa04dda202 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -10,7 +10,7 @@ using osu.Game.Audio; namespace osu.Game.Skinning { /// - /// Provides access to skinnable elements. + /// Provides access to various elements contained by a skin. /// public interface ISkin { diff --git a/osu.Game/Skinning/ISkinComponentLookup.cs b/osu.Game/Skinning/ISkinComponentLookup.cs index be4043d7cf..25ee086707 100644 --- a/osu.Game/Skinning/ISkinComponentLookup.cs +++ b/osu.Game/Skinning/ISkinComponentLookup.cs @@ -4,7 +4,8 @@ namespace osu.Game.Skinning { /// - /// A lookup type which can be used with . + /// The base lookup type to be used with . + /// Should be implemented as necessary to add further criteria to lookups, which are usually consumed by ruleset transformers or legacy lookup cases. /// /// /// Implementations of should match on types implementing this interface diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs index 89f656a12c..0dfc99e3bc 100644 --- a/osu.Game/Skinning/ISkinSource.cs +++ b/osu.Game/Skinning/ISkinSource.cs @@ -7,8 +7,16 @@ using System.Collections.Generic; namespace osu.Game.Skinning { /// - /// Provides access to skinnable elements. + /// An abstract skin implementation, whose primary purpose is to properly handle component fallback across multiple layers of skins (e.g.: beatmap skin, user skin, default skin). /// + /// + /// Common usage is to do an initial lookup via , and use the returned + /// to do further lookups for related components. + /// + /// The initial lookup is used to lock consecutive lookups to the same underlying skin source (as to not get some elements + /// from one skin and others from another, which would be the case if using methods like + /// directly). + /// public interface ISkinSource : ISkin { /// diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index e75fb1e7e9..c99cdba91c 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -8,7 +8,7 @@ using osuTK; namespace osu.Game.Skinning { - public partial class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable + public partial class LegacyAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 8407b144f8..1cbfda16cf 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -45,11 +45,11 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is GlobalSkinComponentLookup targetComponent) + if (lookup is SkinComponentsContainerLookup containerLookup) { - switch (targetComponent.Lookup) + switch (containerLookup.Target) { - case GlobalSkinComponentLookup.LookupType.MainHUDComponents: + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. // therefore keep the check here until fallback default legacy skin is supported. if (!this.HasFont(LegacyFont.Score)) diff --git a/osu.Game/Skinning/LegacyComboCounter.cs b/osu.Game/Skinning/LegacyComboCounter.cs index c132a72001..cd72055fce 100644 --- a/osu.Game/Skinning/LegacyComboCounter.cs +++ b/osu.Game/Skinning/LegacyComboCounter.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Skinning @@ -15,7 +14,7 @@ namespace osu.Game.Skinning /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public partial class LegacyComboCounter : CompositeDrawable, ISkinnableDrawable + public partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable { public Bindable Current { get; } = new BindableInt { MinValue = 0 }; @@ -45,7 +44,7 @@ namespace osu.Game.Skinning private readonly Container counterContainer; /// - /// Hides the combo counter internally without affecting its . + /// Hides the combo counter internally without affecting its . /// /// /// This is used for rulesets that provide their own combo counter and don't want this HUD one to be visible, diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index c3cb9770fa..f785022f84 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Skinning { - public partial class LegacyHealthDisplay : HealthDisplay, ISkinnableDrawable + public partial class LegacyHealthDisplay : HealthDisplay, ISerialisableDrawable { private const double epic_cutoff = 0.5; diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 88e7bbc23a..d8ee6b21de 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -8,7 +8,7 @@ using osuTK; namespace osu.Game.Skinning { - public partial class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable + public partial class LegacyScoreCounter : GameplayScoreCounter, ISerialisableDrawable { protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 284a938bce..e46eaf90c1 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -343,11 +343,15 @@ namespace osu.Game.Skinning switch (lookup) { - case GlobalSkinComponentLookup target: - switch (target.Lookup) + case SkinComponentsContainerLookup containerLookup: + // Only handle global level defaults for now. + if (containerLookup.Ruleset != null) + return null; + + switch (containerLookup.Target) { - case GlobalSkinComponentLookup.LookupType.MainHUDComponents: - var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + return new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); @@ -387,8 +391,6 @@ namespace osu.Game.Skinning new BarHitErrorMeter(), } }; - - return skinnableTargetWrapper; } return null; diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs new file mode 100644 index 0000000000..51b57a000d --- /dev/null +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -0,0 +1,51 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Extensions; + +namespace osu.Game.Skinning +{ + public static class SerialisableDrawableExtensions + { + public static SerialisedDrawableInfo CreateSerialisedInfo(this Drawable component) => new SerialisedDrawableInfo(component); + + public static void ApplySerialisedInfo(this Drawable component, SerialisedDrawableInfo drawableInfo) + { + // todo: can probably make this better via deserialisation directly using a common interface. + component.Position = drawableInfo.Position; + component.Rotation = drawableInfo.Rotation; + component.Scale = drawableInfo.Scale; + component.Anchor = drawableInfo.Anchor; + component.Origin = drawableInfo.Origin; + + if (component is ISerialisableDrawable serialisableDrawable) + { + serialisableDrawable.UsesFixedAnchor = drawableInfo.UsesFixedAnchor; + + foreach (var (_, property) in component.GetSettingsSourceProperties()) + { + var bindable = ((IBindable)property.GetValue(component)!); + + if (!drawableInfo.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue)) + { + // TODO: We probably want to restore default if not included in serialisation information. + // This is not simple to do as SetDefault() is only found in the typed Bindable interface right now. + continue; + } + + serialisableDrawable.CopyAdjustedSetting(bindable, settingValue); + } + } + + if (component is Container container) + { + foreach (var child in drawableInfo.Children) + container.Add(child.CreateInstance()); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs similarity index 56% rename from osu.Game/Screens/Play/HUD/SkinnableInfo.cs rename to osu.Game/Skinning/SerialisedDrawableInfo.cs index 9fdae50615..c515f228f7 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -13,18 +11,23 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Extensions; -using osu.Game.Skinning; +using osu.Game.Rulesets; using osuTK; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Skinning { /// - /// Serialised information governing custom changes to an . + /// Serialised backing data for s. + /// Used for json serialisation in user skins. /// + /// + /// Can be created using . + /// Can also be applied to an existing drawable using . + /// [Serializable] - public class SkinnableInfo + public sealed class SerialisedDrawableInfo { - public Type Type { get; set; } + public Type Type { get; set; } = null!; public Vector2 Position { get; set; } @@ -36,15 +39,15 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } - /// + /// public bool UsesFixedAnchor { get; set; } public Dictionary Settings { get; set; } = new Dictionary(); - public List Children { get; } = new List(); + public List Children { get; } = new List(); [JsonConstructor] - public SkinnableInfo() + public SerialisedDrawableInfo() { } @@ -52,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD /// Construct a new instance populating all attributes from the provided drawable. /// /// The drawable which attributes should be sourced from. - public SkinnableInfo(Drawable component) + public SerialisedDrawableInfo(Drawable component) { Type = component.GetType(); @@ -62,8 +65,8 @@ namespace osu.Game.Screens.Play.HUD Anchor = component.Anchor; Origin = component.Origin; - if (component is ISkinnableDrawable skinnable) - UsesFixedAnchor = skinnable.UsesFixedAnchor; + if (component is ISerialisableDrawable serialisableDrawable) + UsesFixedAnchor = serialisableDrawable.UsesFixedAnchor; foreach (var (_, property) in component.GetSettingsSourceProperties()) { @@ -74,8 +77,8 @@ namespace osu.Game.Screens.Play.HUD if (component is Container container) { - foreach (var child in container.OfType().OfType()) - Children.Add(child.CreateSkinnableInfo()); + foreach (var child in container.OfType().OfType()) + Children.Add(child.CreateSerialisedInfo()); } } @@ -88,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD try { Drawable d = (Drawable)Activator.CreateInstance(Type)!; - d.ApplySkinnableInfo(this); + d.ApplySerialisedInfo(this); return d; } catch (Exception e) @@ -98,13 +101,18 @@ namespace osu.Game.Screens.Play.HUD } } - public static Type[] GetAllAvailableDrawables() + /// + /// Retrieve all types available which support serialisation. + /// + /// The ruleset to filter results to. If null, global components will be returned instead. + public static Type[] GetAllAvailableDrawables(RulesetInfo? ruleset = null) { - return typeof(OsuGame).Assembly.GetTypes() - .Where(t => !t.IsInterface && !t.IsAbstract) - .Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t)) - .OrderBy(t => t.Name) - .ToArray(); + return (ruleset?.CreateInstance().GetType() ?? typeof(OsuGame)) + .Assembly.GetTypes() + .Where(t => !t.IsInterface && !t.IsAbstract && t.IsPublic) + .Where(t => typeof(ISerialisableDrawable).IsAssignableFrom(t)) + .OrderBy(t => t.Name) + .ToArray(); } } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index c55b4e789c..a6250d7488 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -18,7 +18,6 @@ using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -38,9 +37,10 @@ namespace osu.Game.Skinning public SkinConfiguration Configuration { get; set; } - public IDictionary DrawableComponentInfo => drawableComponentInfo; + public IDictionary LayoutInfos => layoutInfos; - private readonly Dictionary drawableComponentInfo = new Dictionary(); + private readonly Dictionary layoutInfos = + new Dictionary(); public abstract ISample? GetSample(ISampleInfo sampleInfo); @@ -101,7 +101,7 @@ namespace osu.Game.Skinning Configuration = new SkinConfiguration(); // skininfo files may be null for default skin. - foreach (GlobalSkinComponentLookup.LookupType skinnableTarget in Enum.GetValues()) + foreach (SkinComponentsContainerLookup.TargetArea skinnableTarget in Enum.GetValues()) { string filename = $"{skinnableTarget}.json"; @@ -114,18 +114,41 @@ namespace osu.Game.Skinning { string jsonContent = Encoding.UTF8.GetString(bytes); - // handle namespace changes... + SkinLayoutInfo? layoutInfo = null; - // can be removed 2023-01-31 - jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); - jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); + try + { + // First attempt to deserialise using the new SkinLayoutInfo format + layoutInfo = JsonConvert.DeserializeObject(jsonContent); + } + catch + { + } - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + // Of note, the migration code below runs on read of skins, but there's nothing to + // force a rewrite after migration. Let's not remove these migration rules until we + // have something in place to ensure we don't end up breaking skins of users that haven't + // manually saved their skin since a change was implemented. - if (deserializedContent == null) - continue; + // If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list. + if (layoutInfo == null) + { + // handle namespace changes... + jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); + jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + + if (deserializedContent == null) + continue; + + layoutInfo = new SkinLayoutInfo(); + layoutInfo.Update(null, deserializedContent.ToArray()); + + Logger.Log($"Ferrying {deserializedContent.Count()} components in {skinnableTarget} to global section of new {nameof(SkinLayoutInfo)} format"); + } + + LayoutInfos[skinnableTarget] = layoutInfo; } catch (Exception ex) { @@ -144,18 +167,21 @@ namespace osu.Game.Skinning /// Remove all stored customisations for the provided target. /// /// The target container to reset. - public void ResetDrawableTarget(ISkinnableTarget targetContainer) + public void ResetDrawableTarget(SkinComponentsContainer targetContainer) { - DrawableComponentInfo.Remove(targetContainer.Target); + LayoutInfos.Remove(targetContainer.Lookup.Target); } /// /// Update serialised information for the provided target. /// /// The target container to serialise to this skin. - public void UpdateDrawableTarget(ISkinnableTarget targetContainer) + public void UpdateDrawableTarget(SkinComponentsContainer targetContainer) { - DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); + if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Target, out var layoutInfo)) + layoutInfos[targetContainer.Lookup.Target] = layoutInfo = new SkinLayoutInfo(); + + layoutInfo.Update(targetContainer.Lookup.Ruleset, ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray()); } public virtual Drawable? GetDrawableComponent(ISkinComponentLookup lookup) @@ -166,19 +192,17 @@ namespace osu.Game.Skinning case SkinnableSprite.SpriteComponentLookup sprite: return this.GetAnimation(sprite.LookupName, false, false); - case GlobalSkinComponentLookup target: - if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo)) - return null; + case SkinComponentsContainerLookup containerLookup: - var components = new List(); - - foreach (var i in skinnableInfo) - components.Add(i.CreateInstance()); + // It is important to return null if the user has not configured this yet. + // This allows skin transformers the opportunity to provide default components. + if (!LayoutInfos.TryGetValue(containerLookup.Target, out var layoutInfo)) return null; + if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; return new Container { RelativeSizeAxes = Axes.Both, - Children = components, + ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance()) }; } diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs similarity index 62% rename from osu.Game/Skinning/SkinnableTargetContainer.cs rename to osu.Game/Skinning/SkinComponentsContainer.cs index b21e51c0cf..d18e9023cd 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -8,19 +8,30 @@ using System.Threading; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { - public partial class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget + /// + /// A container which holds many skinnable components, with functionality to add, remove and reload layouts. + /// Used to allow user customisation of skin layouts. + /// + /// + /// This is currently used as a means of serialising skin layouts to files. + /// Currently, one json file in a skin will represent one , containing + /// the output of . + /// + public partial class SkinComponentsContainer : SkinReloadableDrawable, ISerialisableDrawableContainer { private Container? content; - public GlobalSkinComponentLookup.LookupType Target { get; } + /// + /// The lookup criteria which will be used to retrieve components from the active skin. + /// + public SkinComponentsContainerLookup Lookup { get; } - public IBindableList Components => components; + public IBindableList Components => components; - private readonly BindableList components = new BindableList(); + private readonly BindableList components = new BindableList(); public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; // ensure that components are loaded even if the target container is hidden (ie. due to user toggle). @@ -28,12 +39,12 @@ namespace osu.Game.Skinning private CancellationTokenSource? cancellationSource; - public SkinnableTargetContainer(GlobalSkinComponentLookup.LookupType target) + public SkinComponentsContainer(SkinComponentsContainerLookup lookup) { - Target = target; + Lookup = lookup; } - public void Reload(SkinnableInfo[] skinnableInfo) + public void Reload(SerialisedDrawableInfo[] skinnableInfo) { var drawables = new List(); @@ -47,7 +58,7 @@ namespace osu.Game.Skinning }); } - public void Reload() => Reload(CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as Container); + public void Reload() => Reload(CurrentSkin.GetDrawableComponent(Lookup) as Container); public void Reload(Container? componentsContainer) { @@ -55,31 +66,26 @@ namespace osu.Game.Skinning components.Clear(); ComponentsLoaded = false; - if (componentsContainer == null) - return; - - content = componentsContainer; + content = componentsContainer ?? new Container + { + RelativeSizeAxes = Axes.Both + }; cancellationSource?.Cancel(); cancellationSource = null; - if (content != null) + LoadComponentAsync(content, wrapper => { - LoadComponentAsync(content, wrapper => - { - AddInternal(wrapper); - components.AddRange(wrapper.Children.OfType()); - ComponentsLoaded = true; - }, (cancellationSource = new CancellationTokenSource()).Token); - } - else + AddInternal(wrapper); + components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; + }, (cancellationSource = new CancellationTokenSource()).Token); } - /// + /// /// Thrown when attempting to add an element to a target which is not supported by the current skin. /// Thrown if the provided instance is not a . - public void Add(ISkinnableDrawable component) + public void Add(ISerialisableDrawable component) { if (content == null) throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin."); @@ -91,10 +97,10 @@ namespace osu.Game.Skinning components.Add(component); } - /// + /// /// Thrown when attempting to add an element to a target which is not supported by the current skin. /// Thrown if the provided instance is not a . - public void Remove(ISkinnableDrawable component) + public void Remove(ISerialisableDrawable component) { if (content == null) throw new NotSupportedException("Attempting to remove a new component from a target container which is not supported by the current skin."); diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/SkinComponentsContainerLookup.cs new file mode 100644 index 0000000000..812ba5d495 --- /dev/null +++ b/osu.Game/Skinning/SkinComponentsContainerLookup.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 osu.Game.Rulesets; + +namespace osu.Game.Skinning +{ + /// + /// Represents a lookup of a collection of elements that make up a particular skinnable of the game. + /// + public class SkinComponentsContainerLookup : ISkinComponentLookup + { + /// + /// The target area / layer of the game for which skin components will be returned. + /// + public readonly TargetArea Target; + + /// + /// The ruleset for which skin components should be returned. + /// A value means that returned components are global and should be applied for all rulesets. + /// + public readonly RulesetInfo? Ruleset; + + public SkinComponentsContainerLookup(TargetArea target, RulesetInfo? ruleset = null) + { + Target = target; + Ruleset = ruleset; + } + + /// + /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor. + /// + public enum TargetArea + { + MainHUDComponents, + SongSelect + } + } +} diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 8ce265719c..9e2e02876d 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -201,7 +201,7 @@ namespace osu.Game.Skinning } // Then serialise each of the drawable component groups into respective files. - foreach (var drawableInfo in skin.DrawableComponentInfo) + foreach (var drawableInfo in skin.LayoutInfos) { string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); diff --git a/osu.Game/Skinning/SkinLayoutInfo.cs b/osu.Game/Skinning/SkinLayoutInfo.cs new file mode 100644 index 0000000000..115d59b9d0 --- /dev/null +++ b/osu.Game/Skinning/SkinLayoutInfo.cs @@ -0,0 +1,37 @@ +// 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.CodeAnalysis; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.Rulesets; + +namespace osu.Game.Skinning +{ + /// + /// A serialisable model describing layout of a . + /// May contain multiple configurations for different rulesets, each of which should manifest their own as required. + /// + [Serializable] + public class SkinLayoutInfo + { + private const string global_identifier = @"global"; + + [JsonIgnore] + public IEnumerable AllDrawables => DrawableInfo.Values.SelectMany(v => v); + + [JsonProperty] + public Dictionary DrawableInfo { get; set; } = new Dictionary(); + + public bool TryGetDrawableInfo(RulesetInfo? ruleset, [NotNullWhen(true)] out SerialisedDrawableInfo[]? components) => + DrawableInfo.TryGetValue(ruleset?.ShortName ?? global_identifier, out components); + + public void Reset(RulesetInfo? ruleset) => + DrawableInfo.Remove(ruleset?.ShortName ?? global_identifier); + + public void Update(RulesetInfo? ruleset, SerialisedDrawableInfo[] components) => + DrawableInfo[ruleset?.ShortName ?? global_identifier] = components; + } +} diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index afead7b072..2612e0b47c 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -15,8 +15,13 @@ using osu.Game.Audio; namespace osu.Game.Skinning { /// - /// A container which adds a local to the hierarchy. + /// A container which adds a provided to the DI skin lookup hierarchy. /// + /// + /// This container will expose an to its children. + /// The source will first consider the skin provided via the constructor (if any), then fallback + /// to any providers in the parent DI hierarchy. + /// public partial class SkinProvidingContainer : Container, ISkinSource { public event Action? SourceChanged; diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index 1c947a4a84..cef1db4bc0 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -9,7 +9,8 @@ using osu.Framework.Graphics.Pooling; namespace osu.Game.Skinning { /// - /// A drawable which has a callback when the skin changes. + /// A poolable drawable implementation which has a pre-wired callback (see ) that fires + /// once on load and again on any subsequent skin change. /// public abstract partial class SkinReloadableDrawable : PoolableDrawable { diff --git a/osu.Game/Skinning/SkinTransformer.cs b/osu.Game/Skinning/SkinTransformer.cs index e05961d404..ed5b04da1e 100644 --- a/osu.Game/Skinning/SkinTransformer.cs +++ b/osu.Game/Skinning/SkinTransformer.cs @@ -10,6 +10,13 @@ using osu.Game.Audio; namespace osu.Game.Skinning { + /// + /// A default skin transformer, which falls back to the provided skin by default. + /// + /// + /// Implementations of skin transformers should generally derive this class and override + /// individual lookup methods, modifying the lookup flow as required. + /// public abstract class SkinTransformer : ISkinTransformer { public ISkin Skin { get; } diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index c3449562c3..1d97566470 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -21,7 +21,7 @@ namespace osu.Game.Skinning /// /// A skinnable element which uses a single texture backing. /// - public partial class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable + public partial class SkinnableSprite : SkinnableDrawable, ISerialisableDrawable { protected override bool ApplySizeRestrictionsToDefault => true; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 7de2b124e7..e88b827807 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -68,10 +68,14 @@ namespace osu.Game.Skinning switch (lookup) { - case GlobalSkinComponentLookup target: - switch (target.Lookup) + case SkinComponentsContainerLookup containerLookup: + // Only handle global level defaults for now. + if (containerLookup.Ruleset != null) + return null; + + switch (containerLookup.Target) { - case GlobalSkinComponentLookup.LookupType.SongSelect: + case SkinComponentsContainerLookup.TargetArea.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -79,7 +83,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case GlobalSkinComponentLookup.LookupType.MainHUDComponents: + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index c5efdf36b4..2e254f5b95 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -45,13 +45,13 @@ namespace osu.Game.Tests.Visual private void addResetTargetsStep() { - AddStep("reset targets", () => this.ChildrenOfType().ForEach(t => + AddStep("reset targets", () => this.ChildrenOfType().ForEach(t => { LegacySkin.ResetDrawableTarget(t); t.Reload(); })); - AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); + AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); } public partial class SkinProvidingPlayer : TestPlayer diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 0559d80384..aa5b506343 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual protected override bool CheckModsAllowFailure() => allowFail; public ModTestPlayer(ModTestData data, bool allowFail) - : base(false, false) + : base(true, false) { this.allowFail = allowFail; currentTestData = data;