diff --git a/osu.Android.props b/osu.Android.props index dd11804b90..e7f90af5fd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 71b73ec78e..80bb82c769 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Text; using DiscordRPC; using DiscordRPC.Message; using osu.Framework.Allocation; @@ -81,8 +83,8 @@ namespace osu.Desktop if (status.Value is UserStatusOnline && activity.Value != null) { - presence.State = activity.Value.Status; - presence.Details = getDetails(activity.Value); + presence.State = truncate(activity.Value.Status); + presence.Details = truncate(getDetails(activity.Value)); } else { @@ -100,6 +102,27 @@ namespace osu.Desktop client.SetPresence(presence); } + private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); + + private string truncate(string str) + { + if (Encoding.UTF8.GetByteCount(str) <= 128) + return str; + + ReadOnlyMemory strMem = str.AsMemory(); + + do + { + strMem = strMem[..^1]; + } while (Encoding.UTF8.GetByteCount(strMem.Span) + ellipsis_length > 128); + + return string.Create(strMem.Length + 1, strMem, (span, mem) => + { + mem.Span.CopyTo(span); + span[^1] = '…'; + }); + } + private string getDetails(UserActivity activity) { switch (activity) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 036c86ffa3..90a6e609f0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -4,7 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; -using System; +using System.Linq; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { } - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index b329f6cd98..58212e29ef 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Catch new CatchModFlashlight(), }; + case ModType.Conversion: + return new Mod[] + { + new CatchModDifficultyAdjust(), + }; + case ModType.Automation: return new Mod[] { diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs new file mode 100644 index 0000000000..4c0f5d510e --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -0,0 +1,49 @@ +// 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.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModDifficultyAdjust : ModDifficultyAdjust + { + [SettingSource("Fruit Size", "Override a beatmap's set CS.")] + public BindableNumber CircleSize { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + public BindableNumber ApproachRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + protected override void TransferSettings(BeatmapDifficulty difficulty) + { + base.TransferSettings(difficulty); + + CircleSize.Value = CircleSize.Default = difficulty.CircleSize; + ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate; + } + + protected override void ApplySettings(BeatmapDifficulty difficulty) + { + base.ApplySettings(difficulty); + + difficulty.CircleSize = CircleSize.Value; + difficulty.ApproachRate = ApproachRate.Value; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index aac3f914a4..1a77a4944b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private const int max_notes_for_density = 7; - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - public int TargetColumns; public bool Dual; public readonly bool IsForCurrentRuleset; @@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } } + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject); + protected override Beatmap ConvertBeatmap(IBeatmap original) { BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 28971b11c5..b07e1d8f54 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), + new ManiaModDifficultyAdjust(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs new file mode 100644 index 0000000000..0817f8f9fc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -0,0 +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 osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModDifficultyAdjust : ModDifficultyAdjust + { + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs new file mode 100644 index 0000000000..4676f14655 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.IO.Stores; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneLegacyBeatmapSkin : OsuTestScene + { + [Resolved] + private AudioManager audio { get; set; } + + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapComboColours(bool customSkinColoursPresent) + { + ExposedPlayer player = null; + + AddStep("load coloured beatmap", () => player = loadBeatmap(customSkinColoursPresent, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); + } + + [Test] + public void TestBeatmapNoComboColours() + { + ExposedPlayer player = null; + + AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + } + + [Test] + public void TestBeatmapNoComboColoursSkinOverride() + { + ExposedPlayer player = null; + + AddStep("load custom-skin colour", () => player = loadBeatmap(true, false)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is custom user skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + } + + private ExposedPlayer loadBeatmap(bool userHasCustomColours, bool beatmapHasColours) + { + ExposedPlayer player; + + Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours); + Child = new OsuScreenStack(player = new ExposedPlayer(userHasCustomColours)) { RelativeSizeAxes = Axes.Both }; + + return player; + } + + private class ExposedPlayer : Player + { + private readonly bool userHasCustomColours; + + public ExposedPlayer(bool userHasCustomColours) + : base(false, false) + { + this.userHasCustomColours = userHasCustomColours; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new TestSkin(userHasCustomColours)); + return dependencies; + } + + public IReadOnlyList UsableComboColours => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value; + } + + private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly bool hasColours; + + public CustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) + : base(new Beatmap + { + BeatmapInfo = + { + BeatmapSet = new BeatmapSetInfo(), + Ruleset = new OsuRuleset().RulesetInfo, + }, + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }, null, null, audio) + { + this.hasColours = hasColours; + } + + protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + } + + private class TestBeatmapSkin : LegacyBeatmapSkin + { + public static Color4[] Colours { get; } = + { + new Color4(50, 100, 150, 255), + new Color4(40, 80, 120, 255), + }; + + public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) + : base(beatmap, new ResourceStore(), null) + { + if (hasColours) + Configuration.AddComboColours(Colours); + } + } + + private class TestSkin : LegacySkin, ISkinSource + { + public static Color4[] Colours { get; } = + { + new Color4(150, 100, 50, 255), + new Color4(20, 20, 20, 255), + }; + + public TestSkin(bool hasCustomColours) + : base(new SkinInfo(), null, null, string.Empty) + { + if (hasCustomColours) + Configuration.AddComboColours(Colours); + } + + public event Action SourceChanged + { + add { } + remove { } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 02ce77e707..bd9d948782 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Tests EndTime = 6000, }, // placeholder object to avoid hitting the results screen - new HitObject + new HitCircle { StartTime = 99999, } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 7bc14c3e41..147d74c929 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using System.Collections.Generic; using osu.Game.Rulesets.Objects.Types; -using System; +using System.Linq; using osu.Game.Rulesets.Osu.UI; using osu.Framework.Extensions.IEnumerableExtensions; @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { } - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasPosition) }; + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs new file mode 100644 index 0000000000..0514e2ab34 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -0,0 +1,49 @@ +// 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.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModDifficultyAdjust : ModDifficultyAdjust + { + [SettingSource("Circle Size", "Override a beatmap's set CS.")] + public BindableNumber CircleSize { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + public BindableNumber ApproachRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + protected override void TransferSettings(BeatmapDifficulty difficulty) + { + base.TransferSettings(difficulty); + + CircleSize.Value = CircleSize.Default = difficulty.CircleSize; + ApproachRate.Value = ApproachRate.Default = difficulty.ApproachRate; + } + + protected override void ApplySettings(BeatmapDifficulty difficulty) + { + base.ApplySettings(difficulty); + + difficulty.CircleSize = CircleSize.Value; + difficulty.ApproachRate = ApproachRate.Value; + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 57d99df5fb..c8a156dc57 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -130,6 +130,7 @@ namespace osu.Game.Rulesets.Osu return new Mod[] { new OsuModTarget(), + new OsuModDifficultyAdjust(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 96e7722e73..cc9d6e4470 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -39,14 +39,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps private readonly bool isForCurrentRuleset; - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; - public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); } + public override bool CanConvert() => true; + protected override Beatmap ConvertBeatmap(IBeatmap original) { // Rewrite the beatmap info to add the slider velocity multiplier diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs new file mode 100644 index 0000000000..56a73ad7df --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -0,0 +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 osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModDifficultyAdjust : ModDifficultyAdjust + { + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 529dfe765c..5890ed2976 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -105,6 +105,12 @@ namespace osu.Game.Rulesets.Taiko new TaikoModFlashlight(), }; + case ModType.Conversion: + return new Mod[] + { + new TaikoModDifficultyAdjust(), + }; + case ModType.Automation: return new Mod[] { diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 6d7159a825..c6d1f9da29 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Gameplay switch (global) { case GlobalSkinConfiguration.ComboColours: - return SkinUtils.As(new Bindable>(ComboColours)); + return SkinUtils.As(new Bindable>(ComboColours)); } break; diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index f68d49dd3e..cef38bbbb8 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -13,31 +13,22 @@ namespace osu.Game.Tests.Skins [TestFixture] public class LegacySkinDecoderTest { - [TestCase(true)] - [TestCase(false)] - public void TestDecodeSkinColours(bool hasColours) + [Test] + public void TestDecodeSkinColours() { var decoder = new LegacySkinDecoder(); - using (var resStream = TestResources.OpenResource(hasColours ? "skin.ini" : "skin-empty.ini")) + using (var resStream = TestResources.OpenResource("skin.ini")) using (var stream = new LineBufferedReader(resStream)) { var comboColors = decoder.Decode(stream).ComboColours; - - List expectedColors; - - if (hasColours) + var expectedColors = new List { - expectedColors = new List - { - new Color4(142, 199, 255, 255), - new Color4(255, 128, 128, 255), - new Color4(128, 255, 255, 255), - new Color4(100, 100, 100, 100), - }; - } - else - expectedColors = new DefaultSkin().Configuration.ComboColours; + new Color4(142, 199, 255, 255), + new Color4(255, 128, 128, 255), + new Color4(128, 255, 255, 255), + new Color4(100, 100, 100, 100), + }; Assert.AreEqual(expectedColors.Count, comboColors.Count); for (int i = 0; i < expectedColors.Count; i++) @@ -45,6 +36,37 @@ namespace osu.Game.Tests.Skins } } + [Test] + public void TestDecodeEmptySkinColours() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("skin-empty.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var comboColors = decoder.Decode(stream).ComboColours; + var expectedColors = SkinConfiguration.DefaultComboColours; + + Assert.AreEqual(expectedColors.Count, comboColors.Count); + for (int i = 0; i < expectedColors.Count; i++) + Assert.AreEqual(expectedColors[i], comboColors[i]); + } + } + + [Test] + public void TestDecodeEmptySkinColoursNoFallback() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("skin-empty.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var skinConfiguration = decoder.Decode(stream); + skinConfiguration.AllowDefaultComboColoursFallback = false; + Assert.IsNull(skinConfiguration.ComboColours); + } + } + [Test] public void TestDecodeGeneral() { diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 8b9c648442..ed54cc982d 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -21,8 +22,8 @@ namespace osu.Game.Tests.Skins [HeadlessTest] public class TestSceneSkinConfigurationLookup : OsuTestScene { - private LegacySkin source1; - private LegacySkin source2; + private SkinSource source1; + private SkinSource source2; private SkinRequester requester; [SetUp] @@ -94,7 +95,7 @@ namespace osu.Game.Tests.Skins [Test] public void TestGlobalLookup() { - AddAssert("Check combo colours", () => requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.Count > 0); + AddAssert("Check combo colours", () => requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.Count > 0); } [Test] @@ -116,6 +117,28 @@ namespace osu.Game.Tests.Skins }); } + [Test] + public void TestEmptyComboColours() + { + AddAssert("Check retrieved combo colours is skin default colours", () => + requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.SequenceEqual(SkinConfiguration.DefaultComboColours) ?? false); + } + + [Test] + public void TestEmptyComboColoursNoFallback() + { + AddStep("Add custom combo colours to source1", () => source1.Configuration.AddComboColours( + new Color4(100, 150, 200, 255), + new Color4(55, 110, 166, 255), + new Color4(75, 125, 175, 255) + )); + + AddStep("Disallow default colours fallback in source2", () => source2.Configuration.AllowDefaultComboColoursFallback = false); + + AddAssert("Check retrieved combo colours from source1", () => + requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.SequenceEqual(source1.Configuration.ComboColours) ?? false); + } + [Test] public void TestLegacyVersionLookup() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index dc950e43bd..36235a4418 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) }; + public override bool CanConvert() => true; protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs deleted file mode 100644 index 66144cbfe4..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ /dev/null @@ -1,237 +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; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Select; -using osu.Game.Tests.Beatmaps; -using osuTK; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - [System.ComponentModel.Description("PlaySongSelect leaderboard/details area")] - public class TestSceneBeatmapDetailArea : OsuTestScene - { - public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; - - private ModDisplay modDisplay; - - [BackgroundDependencyLoader] - private void load(OsuGameBase game, RulesetStore rulesets) - { - BeatmapDetailArea detailsArea; - Add(detailsArea = new BeatmapDetailArea - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(550f, 450f), - }); - - Add(modDisplay = new ModDisplay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Position = new Vector2(0, 25), - }); - - modDisplay.Current.BindTo(SelectedMods); - - AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "All Metrics", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has all the metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 7, - DrainRate = 1, - OverallDifficulty = 5.7f, - ApproachRate = 3.5f, - }, - StarDifficulty = 5.3f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("all except source", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "All Metrics", - Metadata = new BeatmapMetadata - { - Tags = "this beatmap has all the metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 7, - DrainRate = 1, - OverallDifficulty = 5.7f, - ApproachRate = 3.5f, - }, - StarDifficulty = 5.3f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("ratings", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "Only Ratings", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has ratings metrics but not retries or fails", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 6, - DrainRate = 9, - OverallDifficulty = 6, - ApproachRate = 6, - }, - StarDifficulty = 4.8f - } - })); - - AddStep("fails+retries", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "Only Retries and Fails", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has retries and fails but no ratings", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 3.7f, - DrainRate = 6, - OverallDifficulty = 6, - ApproachRate = 7, - }, - StarDifficulty = 2.91f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("null metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "No Metrics", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has no metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 5, - DrainRate = 5, - OverallDifficulty = 5.5f, - ApproachRate = 6.5f, - }, - StarDifficulty = 1.97f, - } - })); - - AddStep("null beatmap", () => detailsArea.Beatmap = null); - - Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); - - AddStep("with EZ mod", () => - { - detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "Has Easy Mod", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has the easy mod enabled", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 3, - DrainRate = 3, - OverallDifficulty = 3, - ApproachRate = 3, - }, - StarDifficulty = 1f, - } - }); - - SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; - }); - - AddStep("with HR mod", () => - { - detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "Has Hard Rock Mod", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has the hard rock mod enabled", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 3, - DrainRate = 3, - OverallDifficulty = 3, - ApproachRate = 3, - }, - StarDifficulty = 1f, - } - }); - - SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; - }); - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index acf037198f..6aa5a76490 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -3,8 +3,14 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.SongSelect @@ -174,5 +180,27 @@ namespace osu.Game.Tests.Visual.SongSelect OnlineBeatmapID = 162, }); } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [Test] + public void TestModAdjustments() + { + TestAllMetrics(); + + Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); + + AddStep("with EZ mod", () => SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }); + + AddAssert("first bar coloured blue", () => details.ChildrenOfType().Skip(1).First().AccentColour == colours.BlueDark); + + AddStep("with HR mod", () => SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }); + + AddAssert("first bar coloured red", () => details.ChildrenOfType().Skip(1).First().AccentColour == colours.Red); + } } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index e6de1eebcd..99e0bf4e33 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps /// /// Whether can be converted by this . /// - public bool CanConvert => !Beatmap.HitObjects.Any() || ValidConversionTypes.All(t => Beatmap.HitObjects.Any(t.IsInstanceOfType)); + public abstract bool CanConvert(); /// /// Converts . @@ -93,11 +93,6 @@ namespace osu.Game.Beatmaps return result; } - /// - /// The types of HitObjects that can be converted to be used for this Beatmap. - /// - protected abstract IEnumerable ValidConversionTypes { get; } - /// /// Creates the that will be returned by this . /// diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 46efe38d37..bfcc38e4a9 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -76,7 +76,7 @@ namespace osu.Game.Beatmaps public IBeatmap Beatmap { get; set; } - public bool CanConvert => true; + public bool CanConvert() => true; public IBeatmap Convert() { diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs index 4c15cb96d1..41c85db063 100644 --- a/osu.Game/Beatmaps/Formats/IHasComboColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasComboColours.cs @@ -8,6 +8,14 @@ namespace osu.Game.Beatmaps.Formats { public interface IHasComboColours { - List ComboColours { get; set; } + /// + /// Retrieves the list of combo colours for presentation only. + /// + IReadOnlyList ComboColours { get; } + + /// + /// Adds combo colours to the list. + /// + void AddComboColours(params Color4[] colours); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index b1585d04c5..f55e24245b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -77,8 +77,6 @@ namespace osu.Game.Beatmaps.Formats return line; } - private bool hasComboColours; - private void handleColours(T output, string line) { var pair = SplitKeyVal(line); @@ -105,14 +103,7 @@ namespace osu.Game.Beatmaps.Formats { if (!(output is IHasComboColours tHasComboColours)) return; - if (!hasComboColours) - { - // remove default colours. - tHasComboColours.ComboColours.Clear(); - hasComboColours = true; - } - - tHasComboColours.ComboColours.Add(colour); + tHasComboColours.AddComboColours(colour); } else { diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index f2213b85f1..173d5494ba 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps /// /// Whether can be converted by this . /// - bool CanConvert { get; } + bool CanConvert(); /// /// Converts . diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 1255665cf0..6aba5257f5 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -108,7 +108,7 @@ namespace osu.Game.Beatmaps IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); // Check if the beatmap can be converted - if (!converter.CanConvert) + if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); // Apply conversion mods diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1cac4d76ab..f5b7bc3073 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -12,10 +12,12 @@ using osu.Framework.Input.Events; namespace osu.Game.Graphics.UserInterface { - public class OsuTextBox : TextBox + public class OsuTextBox : BasicTextBox { protected override float LeftRightPadding => 10; + protected override float CaretWidth => 3; + protected override SpriteText CreatePlaceholder() => new OsuSpriteText { Font = OsuFont.GetFont(italics: true), @@ -41,6 +43,8 @@ namespace osu.Game.Graphics.UserInterface BackgroundCommit = BorderColour = colour.Yellow; } + protected override Color4 SelectionColour => new Color4(249, 90, 255, 255); + protected override void OnFocus(FocusEvent e) { BorderThickness = 3; diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs new file mode 100644 index 0000000000..e7afa48502 --- /dev/null +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -0,0 +1,13 @@ +// 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.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public interface ISettingsItem : IDrawable, IDisposable + { + event Action SettingChanged; + } +} diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 31fcb7abd8..35f28ab1b2 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.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 System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -20,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable + public abstract class SettingsItem : Container, IFilterable, ISettingsItem { protected abstract Drawable CreateControl(); @@ -34,8 +35,6 @@ namespace osu.Game.Overlays.Settings private SpriteText text; - private readonly RestoreDefaultValueButton restoreDefaultButton; - public bool ShowsDefaultIndicator = true; public virtual string LabelText @@ -70,8 +69,12 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + public event Action SettingChanged; + protected SettingsItem() { + RestoreDefaultValueButton restoreDefaultButton; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS }; @@ -87,13 +90,12 @@ namespace osu.Game.Overlays.Settings Child = Control = CreateControl() }, }; - } - [BackgroundDependencyLoader] - private void load() - { + // all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is + // never loaded, but requires bindable storage. if (controlWithCurrent != null) { + controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke(); controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; if (ShowsDefaultIndicator) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs new file mode 100644 index 0000000000..224fc78508 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -0,0 +1,81 @@ +// 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.Beatmaps; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using System; +using osu.Game.Configuration; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModDifficultyAdjust : Mod, IApplicableToDifficulty + { + public override string Name => @"Difficulty Adjust"; + + public override string Description => @"Override a beatmap's difficulty settings."; + + public override string Acronym => "DA"; + + public override ModType Type => ModType.Conversion; + + public override IconUsage Icon => FontAwesome.Solid.Hammer; + + public override double ScoreMultiplier => 1.0; + + public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModHardRock) }; + + [SettingSource("Drain Rate", "Override a beatmap's set HP.")] + public BindableNumber DrainRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Overall Difficulty", "Override a beatmap's set OD.")] + public BindableNumber OverallDifficulty { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + private BeatmapDifficulty difficulty; + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + if (this.difficulty == null || this.difficulty.ID != difficulty.ID) + { + this.difficulty = difficulty; + TransferSettings(difficulty); + } + else + ApplySettings(difficulty); + } + + /// + /// Transfer initial settings from the beatmap to settings. + /// + /// The beatmap's initial values. + protected virtual void TransferSettings(BeatmapDifficulty difficulty) + { + DrainRate.Value = DrainRate.Default = difficulty.DrainRate; + OverallDifficulty.Value = OverallDifficulty.Default = difficulty.OverallDifficulty; + } + + /// + /// Apply all custom settings to the provided beatmap. + /// + /// The beatmap to have settings applied. + protected virtual void ApplySettings(BeatmapDifficulty difficulty) + { + difficulty.DrainRate = DrainRate.Value; + difficulty.OverallDifficulty = OverallDifficulty.Value; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index a55ebc51d6..a91e4dfd5c 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -19,9 +20,16 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override double ScoreMultiplier => 0.5; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; + public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - private int retries = 2; + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + + private int retries; private BindableNumber health; @@ -32,6 +40,8 @@ namespace osu.Game.Rulesets.Mods difficulty.ApproachRate *= ratio; difficulty.DrainRate *= ratio; difficulty.OverallDifficulty *= ratio; + + retries = Retries.Value; } public bool AllowFail diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 2044cbeae2..2bcac3e4a9 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage Icon => OsuIcon.ModHardrock; public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Everything just got a bit harder..."; - public override Type[] IncompatibleMods => new[] { typeof(ModEasy) }; + public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; public void ApplyToDifficulty(BeatmapDifficulty difficulty) { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 386805d7e5..a959fee9be 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -356,7 +356,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { if (HitObject is IHasComboInformation combo) { - var comboColours = CurrentSkin.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value; + var comboColours = CurrentSkin.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value; AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White; } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 231115d1e1..b28d572b5c 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; @@ -132,6 +133,8 @@ namespace osu.Game.Screens.Menu private void confirmAndExit() { + if (exitConfirmed) return; + exitConfirmed = true; this.Exit(); } @@ -244,10 +247,18 @@ namespace osu.Game.Screens.Menu public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && !(dialogOverlay.CurrentDialog is ConfirmExitDialog)) + if (!exitConfirmed && dialogOverlay != null) { - dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); - return true; + if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) + { + exitConfirmed = true; + exitDialog.Buttons.First().Click(); + } + else + { + dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); + return true; + } } buttons.State = ButtonSystemState.Exit; diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 9c9c33274f..a147527f6c 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -16,6 +16,9 @@ using osu.Framework.Bindables; using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; +using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Select.Details { @@ -69,7 +72,37 @@ namespace osu.Game.Screens.Select.Details { base.LoadComplete(); - mods.BindValueChanged(_ => updateStatistics(), true); + mods.BindValueChanged(modsChanged, true); + } + + private readonly List references = new List(); + + private void modsChanged(ValueChangedEvent> mods) + { + // TODO: find a more permanent solution for this if/when it is needed in other components. + // this is generating drawables for the only purpose of storing bindable references. + foreach (var r in references) + r.Dispose(); + + references.Clear(); + + ScheduledDelegate debounce = null; + + foreach (var mod in mods.NewValue.OfType()) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => + { + debounce?.Cancel(); + debounce = Scheduler.AddDelayed(updateStatistics, 100); + }; + + references.Add(setting); + } + } + + updateStatistics(); } private void updateStatistics() diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 0caf2d19e9..1929a7e5d2 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -13,13 +13,12 @@ namespace osu.Game.Skinning : base(Info, storage, audioManager, string.Empty) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); - Configuration.ComboColours.AddRange(new[] - { + Configuration.AddComboColours( new Color4(255, 192, 0, 255), new Color4(0, 202, 0, 255), new Color4(18, 124, 255, 255), - new Color4(242, 24, 57, 255), - }); + new Color4(242, 24, 57, 255) + ); Configuration.LegacyVersion = 2.0m; } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 529c1afca5..2a065ea3d7 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -35,7 +35,7 @@ namespace osu.Game.Skinning switch (global) { case GlobalSkinConfiguration.ComboColours: - return SkinUtils.As(new Bindable>(Configuration.ComboColours)); + return SkinUtils.As(new Bindable>(Configuration.ComboColours)); } break; diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs index cd5975edac..5842ee82ee 100644 --- a/osu.Game/Skinning/DefaultSkinConfiguration.cs +++ b/osu.Game/Skinning/DefaultSkinConfiguration.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. -using osuTK.Graphics; - namespace osu.Game.Skinning { /// @@ -10,15 +8,5 @@ namespace osu.Game.Skinning /// public class DefaultSkinConfiguration : SkinConfiguration { - public DefaultSkinConfiguration() - { - ComboColours.AddRange(new[] - { - new Color4(255, 192, 0, 255), - new Color4(0, 202, 0, 255), - new Color4(18, 124, 255, 255), - new Color4(242, 24, 57, 255), - }); - } } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 6770da3c66..fa7e895a28 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -12,6 +12,8 @@ namespace osu.Game.Skinning public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) { + // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) + Configuration.AllowDefaultComboColoursFallback = false; } private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 868e3921bb..48c520986a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -72,7 +72,11 @@ namespace osu.Game.Skinning switch (global) { case GlobalSkinConfiguration.ComboColours: - return SkinUtils.As(new Bindable>(Configuration.ComboColours)); + var comboColours = Configuration.ComboColours; + if (comboColours != null) + return SkinUtils.As(new Bindable>(comboColours)); + + break; } break; diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index b1679bd464..027f5b8883 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -3,7 +3,7 @@ namespace osu.Game.Skinning { - public class LegacySkinConfiguration : DefaultSkinConfiguration + public class LegacySkinConfiguration : SkinConfiguration { public const decimal LATEST_VERSION = 2.7m; diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 54aac86e3c..a55870aa6d 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -14,7 +14,36 @@ namespace osu.Game.Skinning { public readonly SkinInfo SkinInfo = new SkinInfo(); - public List ComboColours { get; set; } = new List(); + /// + /// Whether to allow as a fallback list for when no combo colours are provided. + /// + internal bool AllowDefaultComboColoursFallback = true; + + public static List DefaultComboColours { get; } = new List + { + new Color4(255, 192, 0, 255), + new Color4(0, 202, 0, 255), + new Color4(18, 124, 255, 255), + new Color4(242, 24, 57, 255), + }; + + private readonly List comboColours = new List(); + + public IReadOnlyList ComboColours + { + get + { + if (comboColours.Count > 0) + return comboColours; + + if (AllowDefaultComboColoursFallback) + return DefaultComboColours; + + return null; + } + } + + public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); public Dictionary CustomColours { get; set; } = new Dictionary(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 757e0e11fa..0c0a58d533 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0dba92b975..edeeea239e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - +