From bb2fba0f9450ad35184d051d37f37e7f9c125dfc Mon Sep 17 00:00:00 2001 From: Lucy Faria Date: Sat, 1 Aug 2026 23:46:52 -0400 Subject: [PATCH 1/4] add adjustable rhythm-based colouring to mania --- .../Mods/CatchModSynesthesia.cs | 2 +- .../Resources/special-skin/mania-note1@2x.png | Bin 0 -> 2752 bytes .../Resources/special-skin/mania-noteS@2x.png | Bin 0 -> 2752 bytes .../Resources/special-skin/skin.ini | 6 + .../Skinning/ColumnTestContainer.cs | 2 +- .../Skinning/ManiaHitObjectTestScene.cs | 2 +- .../Skinning/ManiaSkinnableTestScene.cs | 29 ----- .../ManiaSkinnableTestSceneScrolling.cs | 46 +++++++ .../TestSceneAdjustableTimingColouring.cs | 118 ++++++++++++++++++ .../Skinning/TestSceneBarLine.cs | 2 +- .../Skinning/TestSceneColumnBackground.cs | 2 +- .../Skinning/TestSceneColumnHitObjectArea.cs | 2 +- .../Skinning/TestSceneComboCounter.cs | 2 +- .../Skinning/TestSceneDrawableJudgement.cs | 2 +- .../Skinning/TestSceneHitExplosion.cs | 2 +- .../Skinning/TestScenePlayfield.cs | 2 +- .../Skinning/TestSceneStage.cs | 2 +- .../Skinning/TestSceneStageBackground.cs | 2 +- .../Skinning/TestSceneStageForeground.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 9 +- .../Mods/OsuModSynesthesia.cs | 2 +- .../Beatmaps/Formats/IHasTimingColours.cs | 24 ++++ osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 15 +++ osu.Game/Screens/Edit/BindableBeatDivisor.cs | 2 +- .../Compose/Components/BeatDivisorControl.cs | 2 +- .../Edit/Compose/Components/BeatSnapGrid.cs | 2 +- .../Compose/Components/DistanceSnapGrid.cs | 2 +- .../Timeline/TimelineTickDisplay.cs | 2 +- osu.Game/Skinning/ArgonSkin.cs | 4 + osu.Game/Skinning/LegacySkin.cs | 3 + osu.Game/Skinning/LegacySkinEncoder.cs | 8 ++ osu.Game/Skinning/SkinConfiguration.cs | 27 +++- osu.Game/Skinning/SkinTimingColourLookup.cs | 18 +++ 33 files changed, 293 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-note1@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-noteS@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs create mode 100644 osu.Game/Beatmaps/Formats/IHasTimingColours.cs create mode 100644 osu.Game/Skinning/SkinTimingColourLookup.cs diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModSynesthesia.cs b/osu.Game.Rulesets.Catch/Mods/CatchModSynesthesia.cs index abad658d94fc..9571ad62f3df 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModSynesthesia.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModSynesthesia.cs @@ -39,7 +39,7 @@ public void ApplyToDrawableHitObject(DrawableHitObject d) // Block bananas from getting coloured. if (d.HitObject is not Banana) { - timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours); + timingBasedColour = BindableBeatDivisor.GetDefaultColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours); } // Colour droplets into a solid colour, as droplets aren't generated snapped to timeline ticks. diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-note1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-note1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797f1dc499653d41727acfd07f99d34e466f0c20 GIT binary patch literal 2752 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 PbLM>3-J$$+z|a2!zyBhF literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-noteS@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-noteS@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797f1dc499653d41727acfd07f99d34e466f0c20 GIT binary patch literal 2752 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 PbLM>3-J$$+z|a2!zyBhF literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 3a9d465f8dc2..c00546dd6285 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -1,6 +1,12 @@ [General] Version: 2.5 +[Colours] +SnapDefault: 255, 255, 255 +Snap1: 255, 0, 0 +Snap2: 0, 0, 255 +Snap3: 0, 255, 0 + [Mania] Keys: 4 ColumnLineWidth: 3,1,3,1,1 diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index 465d4a49f085..620c916879f7 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { /// - /// A container to be used in a to provide a resolvable dependency. + /// A container to be used in a to provide a resolvable dependency. /// public partial class ColumnTestContainer : Container { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index dd494dfc8217..172ed911ebd5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// /// A test scene for a mania hitobject. /// - public abstract partial class ManiaHitObjectTestScene : ManiaSkinnableTestScene + public abstract partial class ManiaHitObjectTestScene : ManiaSkinnableTestSceneScrolling { [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index b2e8ebd5812d..66464b1a4fca 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -1,15 +1,11 @@ // 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.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osu.Game.Tests.Visual; using osuTK.Graphics; @@ -20,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene { - [Cached(Type = typeof(IScrollingInfo))] - protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo(); [Cached] private readonly StageDefinition stage = new StageDefinition(4); @@ -30,8 +24,6 @@ public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene protected ManiaSkinnableTestScene() { - ScrollingInfo.Direction.Value = ScrollingDirection.Down; - Add(new Box { RelativeSizeAxes = Axes.Both, @@ -39,26 +31,5 @@ protected ManiaSkinnableTestScene() Depth = 1 }); } - - [Test] - public void TestScrollingDown() - { - AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); - } - - [Test] - public void TestScrollingUp() - { - AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); - } - - protected class TestScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - - IBindable IScrollingInfo.Direction => Direction; - IBindable IScrollingInfo.TimeRange { get; } = new Bindable(5000); - IBindable IScrollingInfo.Algorithm { get; } = new Bindable(new ConstantScrollAlgorithm()); - } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs new file mode 100644 index 000000000000..599a0c65f134 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A test scene for skinnable mania components, with an provided. + /// + public abstract partial class ManiaSkinnableTestSceneScrolling : ManiaSkinnableTestScene + { + [Cached(Type = typeof(IScrollingInfo))] + protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo(); + + protected ManiaSkinnableTestSceneScrolling() : base() + { + ScrollingInfo.Direction.Value = ScrollingDirection.Down; + } + + [Test] + public void TestScrollingDown() + { + AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); + } + + [Test] + public void TestScrollingUp() + { + AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); + } + + protected class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + + IBindable IScrollingInfo.Direction => Direction; + IBindable IScrollingInfo.TimeRange { get; } = new Bindable(5000); + IBindable IScrollingInfo.Algorithm { get; } = new Bindable(new ConstantScrollAlgorithm()); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs new file mode 100644 index 000000000000..474627e85a81 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs @@ -0,0 +1,118 @@ +// 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.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Framework.Graphics; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Allocation; +using osu.Framework.Timing; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using System.Linq; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + [TestFixture] + public partial class TestSceneAdjustableTimingColouring : ManiaSkinnableTestScene + { + + private Bindable configTimingBasedNoteColouring = null!; + + private ManualClock clock = null!; + + private ManiaBeatmap testBeatmap = null!; + + [BackgroundDependencyLoader] + private void load() + { + testBeatmap = createTestBeatmap(); + SetContents((skin) => + { + var drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(testBeatmap); + drawableRuleset.Clock = new FramedClock(clock = new ManualClock()); + + return drawableRuleset; + }); + var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); + configTimingBasedNoteColouring = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring); + } + [Test] + public void TestColouring() + { + AddStep("disable colouring", () => configTimingBasedNoteColouring.Value = false); + AddStep("enable colouring", () => configTimingBasedNoteColouring.Value = true); + } + [Test] + public void TestCustomColouring() + { + AddStep("disable colouring", () => configTimingBasedNoteColouring.Value = false); + AddAssert("all notes not coloured", () => this.ChildrenOfType().All(note => note.Colour == Colour4.White)); + AddStep("enable colouring", () => configTimingBasedNoteColouring.Value = true); + AddAssert("any notes coloured", () => this.ChildrenOfType().Any(note => note.Colour != Colour4.White)); + AddAssert("special-skin colours correct", + () => Cell(4).ChildrenOfType().All(note => + snapColourIsAccurate( + note.Colour, + testBeatmap.ControlPointInfo.GetClosestBeatDivisor(note.HitObject.StartTime) + )) + ); + } + private bool snapColourIsAccurate(Color4 color, int divisor) + { + switch (divisor) + { + case 1: + return color == new Color4(255, 0, 0, 255); + case 2: + return color == new Color4(0, 0, 255, 255); + case 3: + return color == new Color4(0, 255, 0, 255); + + default: + return color == new Color4(255, 255, 255, 255); + } + } + + private ManiaBeatmap createTestBeatmap() + { + const double beat_length = 1000; + + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note { StartTime = 0 }, + new Note { StartTime = beat_length / 16 }, + new Note { StartTime = beat_length / 12 }, + new Note { StartTime = beat_length / 8 }, + new Note { StartTime = beat_length / 6 }, + new Note { StartTime = beat_length / 4 }, + new Note { StartTime = beat_length / 3 }, + new Note { StartTime = beat_length / 2 }, + new Note { StartTime = beat_length } + }, + ControlPointInfo = new ControlPointInfo(), + BeatmapInfo = { Ruleset = Ruleset.Value }, + }; + + foreach (var note in beatmap.HitObjects) + { + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint + { + BeatLength = beat_length + }); + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs index a5c18babe2a0..275c58f8489e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneBarLine : ManiaSkinnableTestScene + public partial class TestSceneBarLine : ManiaSkinnableTestSceneScrolling { [Test] public void TestMinor() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index 47923d0733d6..203b990f278f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneColumnBackground : ManiaSkinnableTestScene + public partial class TestSceneColumnBackground : ManiaSkinnableTestSceneScrolling { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index bf67d2d6a9c8..e11caa9fda11 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneColumnHitObjectArea : ManiaSkinnableTestScene + public partial class TestSceneColumnHitObjectArea : ManiaSkinnableTestSceneScrolling { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs index ccdebb502c5d..f224af66f6c4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneComboCounter : ManiaSkinnableTestScene + public partial class TestSceneComboCounter : ManiaSkinnableTestSceneScrolling { [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset()); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index b52919987f15..8dfd57a040fa 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneDrawableJudgement : ManiaSkinnableTestScene + public partial class TestSceneDrawableJudgement : ManiaSkinnableTestSceneScrolling { public TestSceneDrawableJudgement() { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index a0833ff91fef..50ccf4ff1d92 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { [TestFixture] - public partial class TestSceneHitExplosion : ManiaSkinnableTestScene + public partial class TestSceneHitExplosion : ManiaSkinnableTestSceneScrolling { private readonly List> hitExplosionPools = new List>(); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index 110336d823c1..c12457de4a30 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestScenePlayfield : ManiaSkinnableTestScene + public partial class TestScenePlayfield : ManiaSkinnableTestSceneScrolling { [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset()); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index 091a4cb55b39..98354570bfe3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneStage : ManiaSkinnableTestScene + public partial class TestSceneStage : ManiaSkinnableTestSceneScrolling { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 11c3ab3cd3d1..752b98e467d3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneStageBackground : ManiaSkinnableTestScene + public partial class TestSceneStageBackground : ManiaSkinnableTestSceneScrolling { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index 04d2eee83e23..b6c461e04c1f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public partial class TestSceneStageForeground : ManiaSkinnableTestScene + public partial class TestSceneStageForeground : ManiaSkinnableTestSceneScrolling { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index f6b92ab4059e..61cfe1535271 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Edit; using osu.Game.Skinning; using osuTK.Graphics; @@ -32,6 +31,9 @@ public partial class DrawableNote : DrawableManiaHitObject, IKeyBindingHan [Resolved(canBeNull: true)] private IBeatmap beatmap { get; set; } + [Resolved] + private ISkinSource skin { get; set; } = null!; + private readonly Bindable configTimingBasedNoteColouring = new Bindable(); protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note; @@ -129,7 +131,10 @@ private void updateSnapColour() int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime); - Colour = configTimingBasedNoteColouring.Value ? BindableBeatDivisor.GetColourFor(snapDivisor, colours) : Color4.White; + Colour = configTimingBasedNoteColouring.Value + ? skin.GetConfig(new SkinTimingColourLookup(snapDivisor)).Value + : Color4.White; + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index e1123807cd94..c35784b6d2be 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -41,7 +41,7 @@ public void ApplyToDrawableHitObject(DrawableHitObject d) double snapTime = d is DrawableSliderTail tail ? tail.Slider.GetEndTime() : d.HitObject.StartTime; - timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours); + timingBasedColour = BindableBeatDivisor.GetDefaultColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours); }; // Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour(). diff --git a/osu.Game/Beatmaps/Formats/IHasTimingColours.cs b/osu.Game/Beatmaps/Formats/IHasTimingColours.cs new file mode 100644 index 000000000000..2b3deb21c94a --- /dev/null +++ b/osu.Game/Beatmaps/Formats/IHasTimingColours.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Formats +{ + public interface IHasTimingColours + { + /// + /// A list of non-default timing colours. + /// null in this context means "unspecified". + /// + /// If index 0 is specified, that should be used as the default + /// for unspecified indexes. + /// + List CustomTimingColours { get; set; } + /// + /// Get the colour corresponding to the specified beat divisor. + /// + Color4 GetTimingColourFor(int beatDivisor); + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 133b1ed12e5b..ac09e32ec704 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -22,6 +22,8 @@ public abstract class LegacyDecoder : Decoder public const int MAX_COMBO_COLOUR_COUNT = 8; + public const int MAX_SNAP_COLOUR_COUNT = 16; + /// /// The .osu format (beatmap) version. /// @@ -140,6 +142,19 @@ protected void HandleColours(TModel output, string line, bool allowAlpha tHasComboColours.CustomComboColours.Add(colour); } + else if (pair.Key.StartsWith(@"Snap", StringComparison.Ordinal)) + { + if (!(output is IHasTimingColours tHasTimingColours)) return; + + if (pair.Key == "SnapDefault") + { + tHasTimingColours.CustomTimingColours[0] = colour; + } + else if (int.TryParse(pair.Key[4..], out int snapIndex) && snapIndex >= 1 && snapIndex <= MAX_SNAP_COLOUR_COUNT) + { + tHasTimingColours.CustomTimingColours[snapIndex] = colour; + } + } else { if (!(output is IHasCustomColours tHasCustomColours)) return; diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index 83acc2622f3d..f1ba89217f68 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -101,7 +101,7 @@ public override void BindTo(Bindable them) /// The beat divisor. /// The set of colours. /// The applicable colour from for . - public static Color4 GetColourFor(int beatDivisor, OsuColour colours) + public static Color4 GetDefaultColourFor(int beatDivisor, OsuColour colours) { switch (beatDivisor) { diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index da145f0994e7..47e07d23cc4d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -425,7 +425,7 @@ private void updateDivisors() Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, RelativePositionAxes = Axes.Both, - Colour = BindableBeatDivisor.GetColourFor(divisor, colours), + Colour = BindableBeatDivisor.GetDefaultColourFor(divisor, colours), X = tickIndex / (float)largestDivisor, }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs index f1b7951999c8..ee359b793358 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs @@ -122,7 +122,7 @@ private void createLines() timingPoint = nextTimingPoint; } - Color4 colour = BindableBeatDivisor.GetColourFor( + Color4 colour = BindableBeatDivisor.GetDefaultColourFor( BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours); foreach (var grid in grids) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 8322c67defa4..32820d3bc9de 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -162,7 +162,7 @@ protected Color4 GetColourForIndexFromPlacement(int placementIndex) if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01)) beatIndex = (int)Math.Floor(fractionalBeatIndex); - var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); + var colour = BindableBeatDivisor.GetDefaultColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); int repeatIndex = placementIndex / beatDivisor.Value; return colour.Opacity(0.5f / (repeatIndex + 1)); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index faefdee09680..f28fe080cc38 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -132,7 +132,7 @@ private void createTicks() int indexInBar = beat % (point.TimeSignature.Numerator * beatDivisor.Value); int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); - var colour = BindableBeatDivisor.GetColourFor(divisor, colours); + var colour = BindableBeatDivisor.GetDefaultColourFor(divisor, colours); // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6d922e14028d..887e861e22b5 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -309,6 +309,10 @@ public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources) case SkinComboColourLookup comboColour: LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); + + case SkinTimingColourLookup timingColour: + LogLookupDebug(this, lookup, LookupDebugType.Hit); + return SkinUtils.As(new Bindable(Configuration.GetTimingColourFor(timingColour.SnapDivisor))); } LogLookupDebug(this, lookup, LookupDebugType.Miss); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 219f909403f9..6988ddbb5dc6 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -107,6 +107,9 @@ protected override void ParseConfigurationStream(Stream stream) case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString() ?? string.Empty)); + case SkinTimingColourLookup timingColour: + return SkinUtils.As(new Bindable(Configuration.GetTimingColourFor(timingColour.SnapDivisor))); + case LegacyManiaSkinConfigurationLookup maniaLookup: if (!AllowManiaConfigLookups) break; diff --git a/osu.Game/Skinning/LegacySkinEncoder.cs b/osu.Game/Skinning/LegacySkinEncoder.cs index 326af9b991e3..76ad8c11b74e 100644 --- a/osu.Game/Skinning/LegacySkinEncoder.cs +++ b/osu.Game/Skinning/LegacySkinEncoder.cs @@ -61,6 +61,14 @@ public void Encode(TextWriter textWriter) writeColour(textWriter, FormattableString.Invariant($@"Combo{i + 1}"), customColour, allowTransparency: false); } + writeColour(textWriter, @"SnapDefault", skin.Configuration.CustomTimingColours.ElementAtOrDefault(0)); + // intentionally starting at 1 as 0 is used for default + for (int i = 1; i < LegacyDecoder.MAX_SNAP_COLOUR_COUNT; i++) + { + Color4? timingColour = i < skin.Configuration.CustomTimingColours.Count ? skin.Configuration.CustomTimingColours.ElementAtOrDefault(i) : null; + writeColour(textWriter, FormattableString.Invariant($@"Snap{i + 1}"), timingColour, allowTransparency: false); + } + foreach (string key in skin.Configuration.CustomColours.Keys) { Color4? customColour = skin.Configuration.CustomColours.GetValueOrDefault(key); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index ca629f4e8a48..b0ad75e11393 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -1,8 +1,11 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps.Formats; +using osu.Game.Graphics; +using osu.Game.Screens.Edit; using osuTK.Graphics; namespace osu.Game.Skinning @@ -10,12 +13,21 @@ namespace osu.Game.Skinning /// /// An empty skin configuration. /// - public class SkinConfiguration : IHasComboColours, IHasCustomColours + public class SkinConfiguration : IHasComboColours, IHasCustomColours, IHasTimingColours { public readonly SkinInfo SkinInfo = new SkinInfo(); public const decimal LATEST_VERSION = 2.7m; + private OsuColour colours; + + public SkinConfiguration() : this(null) + { } + public SkinConfiguration(OsuColour? colours) + { + this.colours = colours ?? new OsuColour(); + } + /// /// Whether to allow as a fallback list for when no combo colours are provided. /// @@ -70,5 +82,16 @@ public IReadOnlyList? ComboColours public Dictionary CustomColours { get; } = new Dictionary(); public readonly Dictionary ConfigDictionary = new Dictionary(); + + public List CustomTimingColours { get; set; } = [.. new Color4?[64]]; + + public Color4 GetTimingColourFor(int beatDivisor) + { + return CustomTimingColours.ElementAtOrDefault(beatDivisor) + // if not specified, use the user-specified default snap colour + ?? CustomTimingColours[0] + // if not specified, use the normal snap colour + ?? BindableBeatDivisor.GetDefaultColourFor(beatDivisor, colours); + } } } diff --git a/osu.Game/Skinning/SkinTimingColourLookup.cs b/osu.Game/Skinning/SkinTimingColourLookup.cs new file mode 100644 index 000000000000..6977ce60a967 --- /dev/null +++ b/osu.Game/Skinning/SkinTimingColourLookup.cs @@ -0,0 +1,18 @@ +// 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 SkinTimingColourLookup + { + /// + /// The divisor to get a color for. + /// + public readonly int SnapDivisor; + + public SkinTimingColourLookup(int snapDivisor) + { + SnapDivisor = snapDivisor; + } + } +} From 550e68265d7038001addcd68e7ee1af1c41f427c Mon Sep 17 00:00:00 2001 From: Lucy Faria Date: Sat, 1 Aug 2026 23:53:39 -0400 Subject: [PATCH 2/4] fix: update snap colouring when skin changes --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 61cfe1535271..b96fc401bca9 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -68,9 +68,14 @@ protected override void LoadComplete() base.LoadComplete(); configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour()); + skin.SourceChanged += updateSnapColour; StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true); } - + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + CurrentSkin.SourceChanged -= updateSnapColour; + } protected override void OnApply() { base.OnApply(); From 39e705ec746cd40eae62f5843b32b575f2d9dbea Mon Sep 17 00:00:00 2001 From: Lucy Faria Date: Sun, 2 Aug 2026 00:10:38 -0400 Subject: [PATCH 3/4] fix: snap colouring works with triangles skin --- osu.Game/Skinning/TrianglesSkin.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index ae3df35383b5..32857af34626 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -230,6 +230,10 @@ public TrianglesSkin(SkinInfo skin, IStorageResourceProvider resources) case SkinComboColourLookup comboColour: LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); + + case SkinTimingColourLookup timingColour: + LogLookupDebug(this, lookup, LookupDebugType.Hit); + return SkinUtils.As(new Bindable(Configuration.GetTimingColourFor(timingColour.SnapDivisor))); } LogLookupDebug(this, lookup, LookupDebugType.Miss); From 09d868e20990c58c7a778c6c4a42f7c252867724 Mon Sep 17 00:00:00 2001 From: Lucy Faria Date: Sun, 2 Aug 2026 01:13:23 -0400 Subject: [PATCH 4/4] code quality fixes --- .../Skinning/ManiaSkinnableTestScene.cs | 1 - .../ManiaSkinnableTestSceneScrolling.cs | 2 +- .../TestSceneAdjustableTimingColouring.cs | 21 +++++++++++-------- .../Objects/Drawables/DrawableNote.cs | 9 +++----- .../Beatmaps/Formats/IHasTimingColours.cs | 1 + osu.Game/Skinning/LegacySkinEncoder.cs | 1 + osu.Game/Skinning/SkinConfiguration.cs | 14 +++++++------ 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 66464b1a4fca..2f73e5a2eec8 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene { - [Cached] private readonly StageDefinition stage = new StageDefinition(4); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs index 599a0c65f134..2f8b1f357056 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestSceneScrolling.cs @@ -17,7 +17,7 @@ public abstract partial class ManiaSkinnableTestSceneScrolling : ManiaSkinnableT [Cached(Type = typeof(IScrollingInfo))] protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo(); - protected ManiaSkinnableTestSceneScrolling() : base() + protected ManiaSkinnableTestSceneScrolling() { ScrollingInfo.Direction.Value = ScrollingDirection.Down; } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs index 474627e85a81..eb1b8ad206c3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneAdjustableTimingColouring.cs @@ -23,33 +23,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [TestFixture] public partial class TestSceneAdjustableTimingColouring : ManiaSkinnableTestScene { - private Bindable configTimingBasedNoteColouring = null!; - private ManualClock clock = null!; - private ManiaBeatmap testBeatmap = null!; [BackgroundDependencyLoader] private void load() { testBeatmap = createTestBeatmap(); - SetContents((skin) => + SetContents(skin => { var drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(testBeatmap); - drawableRuleset.Clock = new FramedClock(clock = new ManualClock()); + drawableRuleset.Clock = new FramedClock(new ManualClock()); return drawableRuleset; }); var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); configTimingBasedNoteColouring = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring); } + [Test] public void TestColouring() { AddStep("disable colouring", () => configTimingBasedNoteColouring.Value = false); AddStep("enable colouring", () => configTimingBasedNoteColouring.Value = true); } + [Test] public void TestCustomColouring() { @@ -59,20 +58,24 @@ public void TestCustomColouring() AddAssert("any notes coloured", () => this.ChildrenOfType().Any(note => note.Colour != Colour4.White)); AddAssert("special-skin colours correct", () => Cell(4).ChildrenOfType().All(note => - snapColourIsAccurate( - note.Colour, - testBeatmap.ControlPointInfo.GetClosestBeatDivisor(note.HitObject.StartTime) - )) + snapColourIsAccurate( + note.Colour, + testBeatmap.ControlPointInfo.GetClosestBeatDivisor(note.HitObject.StartTime) + ) + ) ); } + private bool snapColourIsAccurate(Color4 color, int divisor) { switch (divisor) { case 1: return color == new Color4(255, 0, 0, 255); + case 2: return color == new Color4(0, 0, 255, 255); + case 3: return color == new Color4(0, 255, 0, 255); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index b96fc401bca9..468fc4029c6e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -10,7 +10,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Scoring; @@ -25,9 +24,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public partial class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler { - [Resolved] - private OsuColour colours { get; set; } - [Resolved(canBeNull: true)] private IBeatmap beatmap { get; set; } @@ -71,11 +67,13 @@ protected override void LoadComplete() skin.SourceChanged += updateSnapColour; StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true); } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); CurrentSkin.SourceChanged -= updateSnapColour; } + protected override void OnApply() { base.OnApply(); @@ -137,9 +135,8 @@ private void updateSnapColour() int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime); Colour = configTimingBasedNoteColouring.Value - ? skin.GetConfig(new SkinTimingColourLookup(snapDivisor)).Value + ? skin.GetConfig(new SkinTimingColourLookup(snapDivisor))?.Value ?? Color4.White : Color4.White; - } } } diff --git a/osu.Game/Beatmaps/Formats/IHasTimingColours.cs b/osu.Game/Beatmaps/Formats/IHasTimingColours.cs index 2b3deb21c94a..bb3caf66703b 100644 --- a/osu.Game/Beatmaps/Formats/IHasTimingColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasTimingColours.cs @@ -16,6 +16,7 @@ public interface IHasTimingColours /// for unspecified indexes. /// List CustomTimingColours { get; set; } + /// /// Get the colour corresponding to the specified beat divisor. /// diff --git a/osu.Game/Skinning/LegacySkinEncoder.cs b/osu.Game/Skinning/LegacySkinEncoder.cs index 76ad8c11b74e..f27adaf9c390 100644 --- a/osu.Game/Skinning/LegacySkinEncoder.cs +++ b/osu.Game/Skinning/LegacySkinEncoder.cs @@ -62,6 +62,7 @@ public void Encode(TextWriter textWriter) } writeColour(textWriter, @"SnapDefault", skin.Configuration.CustomTimingColours.ElementAtOrDefault(0)); + // intentionally starting at 1 as 0 is used for default for (int i = 1; i < LegacyDecoder.MAX_SNAP_COLOUR_COUNT; i++) { diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index b0ad75e11393..24250ace55f5 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -19,10 +19,12 @@ public class SkinConfiguration : IHasComboColours, IHasCustomColours, IHasTiming public const decimal LATEST_VERSION = 2.7m; - private OsuColour colours; + private readonly OsuColour colours; - public SkinConfiguration() : this(null) + public SkinConfiguration() + : this(null) { } + public SkinConfiguration(OsuColour? colours) { this.colours = colours ?? new OsuColour(); @@ -88,10 +90,10 @@ public IReadOnlyList? ComboColours public Color4 GetTimingColourFor(int beatDivisor) { return CustomTimingColours.ElementAtOrDefault(beatDivisor) - // if not specified, use the user-specified default snap colour - ?? CustomTimingColours[0] - // if not specified, use the normal snap colour - ?? BindableBeatDivisor.GetDefaultColourFor(beatDivisor, colours); + // if not specified, use the user-specified default snap colour + ?? CustomTimingColours[0] + // if not specified, use the normal snap colour + ?? BindableBeatDivisor.GetDefaultColourFor(beatDivisor, colours); } } }