diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModEasy.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModEasy.cs new file mode 100644 index 0000000000..1b5d9da02b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModEasy.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModEasy : OsuModTestScene + { + protected override bool AllowFail => true; + + [Test] + public void TestMultipleApplication() + { + bool reapplied = false; + CreateModTest(new ModTestData + { + Mods = [new OsuModEasy { Retries = { Value = 1 } }], + Autoplay = false, + CreateBeatmap = () => + { + // do stuff to speed up fails + var b = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + Difficulty = { DrainRate = 10 } + }; + + foreach (var ho in b.HitObjects) + ho.StartTime /= 4; + + return b; + }, + PassCondition = () => + { + if (((ModEasyTestPlayer)Player).FailuresSuppressed > 0 && !reapplied) + { + try + { + foreach (var mod in Player.GameplayState.Mods.OfType()) + mod.ApplyToDifficulty(new BeatmapDifficulty()); + + foreach (var mod in Player.GameplayState.Mods.OfType()) + mod.ApplyToPlayer(Player); + } + catch + { + // don't care if this fails. in fact a failure here is probably better than the alternative. + } + finally + { + reapplied = true; + } + } + + return Player.GameplayState.HasFailed && ((ModEasyTestPlayer)Player).FailuresSuppressed <= 1; + } + }); + } + + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModEasyTestPlayer(CurrentTestData, AllowFail); + + private partial class ModEasyTestPlayer : ModTestPlayer + { + public int FailuresSuppressed { get; private set; } + + public ModEasyTestPlayer(ModTestData data, bool allowFail) + : base(data, allowFail) + { + } + + protected override bool CheckModsAllowFailure() + { + bool failureAllowed = GameplayState.Mods.OfType().All(m => m.PerformFail()); + + if (!failureAllowed) + FailuresSuppressed++; + + return failureAllowed; + } + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index 1a2cb08a53..09086f4d86 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -3,17 +3,18 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Humanizer; using osu.Framework.Bindables; using osu.Framework.Localisation; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Mods { - public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToPlayer, IApplicableToHealthProcessor { [SettingSource("Extra Lives", "Number of extra lives")] public Bindable Retries { get; } = new BindableInt(2) @@ -33,18 +34,26 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); - private int retries; + private int? retries; private readonly BindableNumber health = new BindableDouble(); - public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + public void ApplyToPlayer(Player player) { - base.ApplyToDifficulty(difficulty); + // this throw works for two reasons: + // - every time `Player` loads, it deep-clones mods into itself, and the deep clone copies *only* `[SettingsSource]` properties + // - `Player` is the only consumer of `IApplicableToPlayer` and it calls `ApplyToPlayer()` exactly once per mod instance + // if either of the above assumptions no longer holds true for any reason, this will need to be reconsidered + if (retries != null) + throw new InvalidOperationException(@"Cannot apply this mod instance to a player twice."); + retries = Retries.Value; } public bool PerformFail() { + Debug.Assert(retries != null); + if (retries == 0) return true; health.Value = health.MaxValue;