mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 08:52:55 +08:00
Merge pull request #13824 from peppy/da-mod-refactor
Refactor `ModDifficultyAdjust` to more elegantly track user override status
This commit is contained in:
commit
0c52b26d23
@ -12,37 +12,29 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
|
||||
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||
};
|
||||
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
|
||||
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
protected override void ApplyLimits(bool extended)
|
||||
{
|
||||
base.ApplyLimits(extended);
|
||||
|
||||
CircleSize.MaxValue = extended ? 11 : 10;
|
||||
ApproachRate.MaxValue = extended ? 11 : 10;
|
||||
}
|
||||
|
||||
public override string SettingDescription
|
||||
{
|
||||
get
|
||||
@ -61,20 +53,12 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TransferSettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.TransferSettings(difficulty);
|
||||
|
||||
TransferSetting(CircleSize, difficulty.CircleSize);
|
||||
TransferSetting(ApproachRate, difficulty.ApproachRate);
|
||||
}
|
||||
|
||||
protected override void ApplySettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplySettings(difficulty);
|
||||
|
||||
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
|
||||
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||
}
|
||||
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -11,34 +10,26 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
|
||||
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||
};
|
||||
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
|
||||
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
protected override void ApplyLimits(bool extended)
|
||||
{
|
||||
base.ApplyLimits(extended);
|
||||
|
||||
CircleSize.MaxValue = extended ? 11 : 10;
|
||||
ApproachRate.MaxValue = extended ? 11 : 10;
|
||||
}
|
||||
|
||||
public override string SettingDescription
|
||||
{
|
||||
get
|
||||
@ -55,20 +46,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TransferSettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.TransferSettings(difficulty);
|
||||
|
||||
TransferSetting(CircleSize, difficulty.CircleSize);
|
||||
TransferSetting(ApproachRate, difficulty.ApproachRate);
|
||||
}
|
||||
|
||||
protected override void ApplySettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplySettings(difficulty);
|
||||
|
||||
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
|
||||
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -11,14 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
|
||||
public BindableNumber<float> ScrollSpeed { get; } = new BindableFloat
|
||||
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable ScrollSpeed { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.05f,
|
||||
MinValue = 0.25f,
|
||||
MaxValue = 4,
|
||||
Default = 1,
|
||||
Value = 1,
|
||||
ReadCurrentFromDifficulty = _ => 1,
|
||||
};
|
||||
|
||||
public override string SettingDescription
|
||||
@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
base.ApplySettings(difficulty);
|
||||
|
||||
ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
|
||||
if (ScrollSpeed.Value != null) difficulty.SliderMultiplier *= ScrollSpeed.Value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
165
osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
Normal file
165
osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
{
|
||||
[TestFixture]
|
||||
public class ModDifficultyAdjustTest
|
||||
{
|
||||
private TestModDifficultyAdjust testMod;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
testMod = new TestModDifficultyAdjust();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnchangedSettingsFollowAppliedDifficulty()
|
||||
{
|
||||
var result = applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 10,
|
||||
OverallDifficulty = 10
|
||||
});
|
||||
|
||||
Assert.That(result.DrainRate, Is.EqualTo(10));
|
||||
Assert.That(result.OverallDifficulty, Is.EqualTo(10));
|
||||
|
||||
result = applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 1,
|
||||
OverallDifficulty = 1
|
||||
});
|
||||
|
||||
Assert.That(result.DrainRate, Is.EqualTo(1));
|
||||
Assert.That(result.OverallDifficulty, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangedSettingsOverrideAppliedDifficulty()
|
||||
{
|
||||
testMod.OverallDifficulty.Value = 4;
|
||||
|
||||
var result = applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 10,
|
||||
OverallDifficulty = 10
|
||||
});
|
||||
|
||||
Assert.That(result.DrainRate, Is.EqualTo(10));
|
||||
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
|
||||
|
||||
result = applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 1,
|
||||
OverallDifficulty = 1
|
||||
});
|
||||
|
||||
Assert.That(result.DrainRate, Is.EqualTo(1));
|
||||
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangedSettingsRetainedWhenSameValueIsApplied()
|
||||
{
|
||||
testMod.OverallDifficulty.Value = 4;
|
||||
|
||||
// Apply and de-apply the same value as the mod.
|
||||
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
|
||||
var result = applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 10 });
|
||||
|
||||
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangedSettingSerialisedWhenSameValueIsApplied()
|
||||
{
|
||||
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
|
||||
testMod.OverallDifficulty.Value = 4;
|
||||
|
||||
var result = (TestModDifficultyAdjust)new APIMod(testMod).ToMod(new TestRuleset());
|
||||
|
||||
Assert.That(result.OverallDifficulty.Value, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangedSettingsRevertedToDefault()
|
||||
{
|
||||
applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 10,
|
||||
OverallDifficulty = 10
|
||||
});
|
||||
|
||||
testMod.OverallDifficulty.Value = 4;
|
||||
testMod.ResetSettingsToDefaults();
|
||||
|
||||
Assert.That(testMod.DrainRate.Value, Is.Null);
|
||||
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
|
||||
|
||||
var applied = applyDifficulty(new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 10,
|
||||
OverallDifficulty = 10
|
||||
});
|
||||
|
||||
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
|
||||
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.
|
||||
/// </summary>
|
||||
private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
// ensure that ReadFromDifficulty doesn't pollute the values.
|
||||
var newDifficulty = difficulty.Clone();
|
||||
|
||||
testMod.ReadFromDifficulty(difficulty);
|
||||
|
||||
testMod.ApplyToDifficulty(newDifficulty);
|
||||
return newDifficulty;
|
||||
}
|
||||
|
||||
private class TestModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
}
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
{
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
if (type == ModType.DifficultyIncrease)
|
||||
yield return new TestModDifficultyAdjust();
|
||||
}
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override string Description => string.Empty;
|
||||
public override string ShortName => string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneModDifficultyAdjustSettings : OsuManualInputManagerTestScene
|
||||
{
|
||||
private OsuModDifficultyAdjust modDifficultyAdjust;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create control", () =>
|
||||
{
|
||||
modDifficultyAdjust = new OsuModDifficultyAdjust();
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
Size = new Vector2(300),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ChildrenEnumerable = modDifficultyAdjust.CreateSettingsControls(),
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFollowsBeatmapDefaultsVisually()
|
||||
{
|
||||
setBeatmapWithDifficultyParameters(5);
|
||||
|
||||
checkSliderAtValue("Circle Size", 5);
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
|
||||
setBeatmapWithDifficultyParameters(8);
|
||||
|
||||
checkSliderAtValue("Circle Size", 8);
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfRangeValueStillApplied()
|
||||
{
|
||||
AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
|
||||
// this is a no-op, just showing that it won't reset the value during deserialisation.
|
||||
setExtendedLimits(false);
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
|
||||
// setting extended limits will reset the serialisation exception.
|
||||
// this should be fine as the goal is to allow, at most, the value of extended limits.
|
||||
setExtendedLimits(true);
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExtendedLimits()
|
||||
{
|
||||
setSliderValue("Circle Size", 99);
|
||||
|
||||
checkSliderAtValue("Circle Size", 10);
|
||||
checkBindableAtValue("Circle Size", 10);
|
||||
|
||||
setExtendedLimits(true);
|
||||
|
||||
checkSliderAtValue("Circle Size", 10);
|
||||
checkBindableAtValue("Circle Size", 10);
|
||||
|
||||
setSliderValue("Circle Size", 99);
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
|
||||
setExtendedLimits(false);
|
||||
|
||||
checkSliderAtValue("Circle Size", 10);
|
||||
checkBindableAtValue("Circle Size", 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserOverrideMaintainedOnBeatmapChange()
|
||||
{
|
||||
setSliderValue("Circle Size", 9);
|
||||
|
||||
setBeatmapWithDifficultyParameters(2);
|
||||
|
||||
checkSliderAtValue("Circle Size", 9);
|
||||
checkBindableAtValue("Circle Size", 9);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestResetToDefault()
|
||||
{
|
||||
setBeatmapWithDifficultyParameters(2);
|
||||
|
||||
setSliderValue("Circle Size", 9);
|
||||
checkSliderAtValue("Circle Size", 9);
|
||||
checkBindableAtValue("Circle Size", 9);
|
||||
|
||||
resetToDefault("Circle Size");
|
||||
checkSliderAtValue("Circle Size", 2);
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserOverrideMaintainedOnMatchingBeatmapValue()
|
||||
{
|
||||
setBeatmapWithDifficultyParameters(3);
|
||||
|
||||
checkSliderAtValue("Circle Size", 3);
|
||||
checkBindableAtValue("Circle Size", null);
|
||||
|
||||
// need to initially change it away from the current beatmap value to trigger an override.
|
||||
setSliderValue("Circle Size", 4);
|
||||
setSliderValue("Circle Size", 3);
|
||||
|
||||
checkSliderAtValue("Circle Size", 3);
|
||||
checkBindableAtValue("Circle Size", 3);
|
||||
|
||||
setBeatmapWithDifficultyParameters(4);
|
||||
|
||||
checkSliderAtValue("Circle Size", 3);
|
||||
checkBindableAtValue("Circle Size", 3);
|
||||
}
|
||||
|
||||
private void resetToDefault(string name)
|
||||
{
|
||||
AddStep($"Reset {name} to default", () =>
|
||||
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
|
||||
.Current.SetDefault());
|
||||
}
|
||||
|
||||
private void setExtendedLimits(bool status) =>
|
||||
AddStep($"Set extended limits {status}", () => modDifficultyAdjust.ExtendedLimits.Value = status);
|
||||
|
||||
private void setSliderValue(string name, float value)
|
||||
{
|
||||
AddStep($"Set {name} slider to {value}", () =>
|
||||
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
|
||||
.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = value);
|
||||
}
|
||||
|
||||
private void checkBindableAtValue(string name, float? expectedValue)
|
||||
{
|
||||
AddAssert($"Bindable {name} is {(expectedValue?.ToString() ?? "null")}", () =>
|
||||
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
|
||||
.Current.Value == expectedValue);
|
||||
}
|
||||
|
||||
private void checkSliderAtValue(string name, float expectedValue)
|
||||
{
|
||||
AddAssert($"Slider {name} at {expectedValue}", () =>
|
||||
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
|
||||
.ChildrenOfType<SettingsSlider<float>>().First().Current.Value == expectedValue);
|
||||
}
|
||||
|
||||
private void setBeatmapWithDifficultyParameters(float value)
|
||||
{
|
||||
AddStep($"set beatmap with all {value}", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = value,
|
||||
CircleSize = value,
|
||||
DrainRate = value,
|
||||
ApproachRate = value,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("show", () => modSelect.Show());
|
||||
|
||||
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
|
||||
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 5);
|
||||
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -101,10 +101,10 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public event Action SettingChanged;
|
||||
|
||||
private readonly RestoreDefaultValueButton<T> restoreDefaultButton;
|
||||
|
||||
protected SettingsItem()
|
||||
{
|
||||
RestoreDefaultValueButton<T> restoreDefaultButton;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS };
|
||||
@ -126,14 +126,19 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
// 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 += _ => updateDisabled();
|
||||
if (controlWithCurrent == null)
|
||||
throw new ArgumentException(@$"Control created via {nameof(CreateControl)} must implement {nameof(IHasCurrentValue<T>)}");
|
||||
|
||||
if (ShowsDefaultIndicator)
|
||||
restoreDefaultButton.Current = controlWithCurrent.Current;
|
||||
}
|
||||
controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke();
|
||||
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (ShowsDefaultIndicator)
|
||||
restoreDefaultButton.Current = controlWithCurrent.Current;
|
||||
}
|
||||
|
||||
private void updateDisabled()
|
||||
|
112
osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
Normal file
112
osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class DifficultyAdjustSettingsControl : SettingsItem<float?>
|
||||
{
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to track the display value on the setting slider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the mod is overriding a default, this will match the value of <see cref="Current"/>.
|
||||
/// When there is no override (ie. <see cref="Current"/> is null), this value will match the beatmap provided default via <see cref="updateCurrentFromSlider"/>.
|
||||
/// </remarks>
|
||||
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
|
||||
|
||||
protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent);
|
||||
|
||||
/// <summary>
|
||||
/// Guards against beatmap values displayed on slider bars being transferred to user override.
|
||||
/// </summary>
|
||||
private bool isInternalChange;
|
||||
|
||||
private DifficultyBindable difficultyBindable;
|
||||
|
||||
public override Bindable<float?> Current
|
||||
{
|
||||
get => base.Current;
|
||||
set
|
||||
{
|
||||
// Intercept and extract the internal number bindable from DifficultyBindable.
|
||||
// This will provide bounds and precision specifications for the slider bar.
|
||||
difficultyBindable = ((DifficultyBindable)value).GetBoundCopy();
|
||||
sliderDisplayCurrent.BindTo(difficultyBindable.CurrentNumber);
|
||||
|
||||
base.Current = difficultyBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(current => updateCurrentFromSlider());
|
||||
beatmap.BindValueChanged(b => updateCurrentFromSlider(), true);
|
||||
|
||||
sliderDisplayCurrent.BindValueChanged(number =>
|
||||
{
|
||||
// this handles the transfer of the slider value to the main bindable.
|
||||
// as such, should be skipped if the slider is being updated via updateFromDifficulty().
|
||||
if (!isInternalChange)
|
||||
Current.Value = number.NewValue;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCurrentFromSlider()
|
||||
{
|
||||
if (Current.Value != null)
|
||||
{
|
||||
// a user override has been added or updated.
|
||||
sliderDisplayCurrent.Value = Current.Value.Value;
|
||||
return;
|
||||
}
|
||||
|
||||
var difficulty = beatmap.Value.BeatmapInfo.BaseDifficulty;
|
||||
|
||||
if (difficulty == null)
|
||||
return;
|
||||
|
||||
// generally should always be implemented, else the slider will have a zero default.
|
||||
if (difficultyBindable.ReadCurrentFromDifficulty == null)
|
||||
return;
|
||||
|
||||
isInternalChange = true;
|
||||
sliderDisplayCurrent.Value = difficultyBindable.ReadCurrentFromDifficulty(difficulty);
|
||||
isInternalChange = false;
|
||||
}
|
||||
|
||||
private class SliderControl : CompositeDrawable, IHasCurrentValue<float?>
|
||||
{
|
||||
// This is required as SettingsItem relies heavily on this bindable for internal use.
|
||||
// The actual update flow is done via the bindable provided in the constructor.
|
||||
public Bindable<float?> Current { get; set; } = new Bindable<float?>();
|
||||
|
||||
public SliderControl(BindableNumber<float> currentNumber)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
ShowsDefaultIndicator = false,
|
||||
Current = currentNumber,
|
||||
}
|
||||
};
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
107
osu.Game/Rulesets/Mods/DifficultyBindable.cs
Normal file
107
osu.Game/Rulesets/Mods/DifficultyBindable.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class DifficultyBindable : Bindable<float?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the extended limits should be applied to this bindable.
|
||||
/// </summary>
|
||||
public readonly BindableBool ExtendedLimits = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// An internal numeric bindable to hold and propagate min/max/precision.
|
||||
/// The value of this bindable should not be set.
|
||||
/// </summary>
|
||||
internal readonly BindableFloat CurrentNumber = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
|
||||
/// </summary>
|
||||
public Func<BeatmapDifficulty, float> ReadCurrentFromDifficulty;
|
||||
|
||||
public float Precision
|
||||
{
|
||||
set => CurrentNumber.Precision = value;
|
||||
}
|
||||
|
||||
public float MinValue
|
||||
{
|
||||
set => CurrentNumber.MinValue = value;
|
||||
}
|
||||
|
||||
private float maxValue;
|
||||
|
||||
public float MaxValue
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == maxValue)
|
||||
return;
|
||||
|
||||
maxValue = value;
|
||||
updateMaxValue();
|
||||
}
|
||||
}
|
||||
|
||||
private float? extendedMaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value to be used when extended limits are applied.
|
||||
/// </summary>
|
||||
public float? ExtendedMaxValue
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == extendedMaxValue)
|
||||
return;
|
||||
|
||||
extendedMaxValue = value;
|
||||
updateMaxValue();
|
||||
}
|
||||
}
|
||||
|
||||
public DifficultyBindable()
|
||||
{
|
||||
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
|
||||
}
|
||||
|
||||
public override float? Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
// Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated.
|
||||
if (value != null)
|
||||
CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value);
|
||||
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMaxValue()
|
||||
{
|
||||
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
|
||||
}
|
||||
|
||||
public new DifficultyBindable GetBoundCopy() => new DifficultyBindable
|
||||
{
|
||||
BindTarget = this,
|
||||
CurrentNumber = { BindTarget = CurrentNumber },
|
||||
ExtendedLimits = { BindTarget = ExtendedLimits },
|
||||
ReadCurrentFromDifficulty = ReadCurrentFromDifficulty,
|
||||
// the following is only safe as long as these values are effectively constants.
|
||||
MaxValue = maxValue,
|
||||
ExtendedMaxValue = extendedMaxValue
|
||||
};
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -33,24 +32,24 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
protected const int LAST_SETTING_ORDER = 2;
|
||||
|
||||
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
|
||||
public BindableNumber<float> DrainRate { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable DrainRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.DrainRate,
|
||||
};
|
||||
|
||||
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
|
||||
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
|
||||
};
|
||||
|
||||
[SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
|
||||
@ -58,17 +57,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
protected ModDifficultyAdjust()
|
||||
{
|
||||
ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the difficulty adjustment limits. Occurs when the value of <see cref="ExtendedLimits"/> is changed.
|
||||
/// </summary>
|
||||
/// <param name="extended">Whether limits should extend beyond sane ranges.</param>
|
||||
protected virtual void ApplyLimits(bool extended)
|
||||
{
|
||||
DrainRate.MaxValue = extended ? 11 : 10;
|
||||
OverallDifficulty.MaxValue = extended ? 11 : 10;
|
||||
foreach (var (_, property) in this.GetOrderedSettingsSourceProperties())
|
||||
{
|
||||
if (property.GetValue(this) is DifficultyBindable diffAdjustBindable)
|
||||
diffAdjustBindable.ExtendedLimits.BindTo(ExtendedLimits);
|
||||
}
|
||||
}
|
||||
|
||||
public override string SettingDescription
|
||||
@ -86,146 +79,20 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
private BeatmapDifficulty difficulty;
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
if (this.difficulty == null || this.difficulty.ID != difficulty.ID)
|
||||
{
|
||||
TransferSettings(difficulty);
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty);
|
||||
|
||||
/// <summary>
|
||||
/// Transfer initial settings from the beatmap to settings.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap's initial values.</param>
|
||||
protected virtual void TransferSettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
TransferSetting(DrainRate, difficulty.DrainRate);
|
||||
TransferSetting(OverallDifficulty, difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
private readonly Dictionary<IBindable, bool> userChangedSettings = new Dictionary<IBindable, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable.
|
||||
/// Only performs the transfer if the user is not currently overriding.
|
||||
/// </summary>
|
||||
protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault)
|
||||
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
|
||||
{
|
||||
bindable.UnbindEvents();
|
||||
|
||||
userChangedSettings.TryAdd(bindable, false);
|
||||
|
||||
bindable.Default = beatmapDefault;
|
||||
|
||||
// users generally choose a difficulty setting and want it to stick across multiple beatmap changes.
|
||||
// we only want to value transfer if the user hasn't changed the value previously.
|
||||
if (!userChangedSettings[bindable])
|
||||
bindable.Value = beatmapDefault;
|
||||
|
||||
bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
|
||||
}
|
||||
|
||||
internal override void CopyAdjustedSetting(IBindable target, object source)
|
||||
{
|
||||
// if the value is non-bindable, it's presumably coming from an external source (like the API) - therefore presume it is not default.
|
||||
// if the value is bindable, defer to the source's IsDefault to be able to tell.
|
||||
userChangedSettings[target] = !(source is IBindable bindableSource) || !bindableSource.IsDefault;
|
||||
base.CopyAdjustedSetting(target, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a setting from a configuration bindable using <paramref name="applyFunc"/>, if it has been changed by the user.
|
||||
/// </summary>
|
||||
protected void ApplySetting<T>(BindableNumber<T> setting, Action<T> applyFunc)
|
||||
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
|
||||
{
|
||||
if (userChangedSettings.TryGetValue(setting, out bool userChangedSetting) && userChangedSetting)
|
||||
applyFunc.Invoke(setting.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply all custom settings to the provided beatmap.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap to have settings applied.</param>
|
||||
protected virtual void ApplySettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
|
||||
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
|
||||
}
|
||||
|
||||
public override void ResetSettingsToDefaults()
|
||||
{
|
||||
base.ResetSettingsToDefaults();
|
||||
|
||||
if (difficulty != null)
|
||||
{
|
||||
// base implementation potentially overwrite modified defaults that came from a beatmap selection.
|
||||
TransferSettings(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableDouble"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableDoubleWithLimitExtension : BindableDouble
|
||||
{
|
||||
public override double Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableFloat"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableFloatWithLimitExtension : BindableFloat
|
||||
{
|
||||
public override float Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableInt"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableIntWithLimitExtension : BindableInt
|
||||
{
|
||||
public override int Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
|
||||
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user