1
0
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:
Dan Balasescu 2021-07-12 12:03:45 +09:00 committed by GitHub
commit 0c52b26d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 649 additions and 220 deletions

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View 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;
}
}
}

View File

@ -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,
}
}
}));
}
}
}

View File

@ -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]

View File

@ -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()

View 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;
}
}
}
}

View 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
};
}
}

View File

@ -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;
}
}
}