Fix player restarting on fail while the triggering mod is not configured as such
Should fix https://github.com/ppy/osu/issues/28434
Not sure if those mods should just have one bool for that? but you could argue this gives more flexibility to player.
Alternatively I did separate branch from scratch https://github.com/Peter-io/osu/tree/alternative-fail-trigger which solves problems mentioned above in simplier way without replacing type of FailConditions and keeping things mostly the same.
Could use this one for PR instead, some likely changes would be to replace object type to something else for FailTriggers list, unless anything could become a trigger in future and not just mods?
Proposal 1: two methods
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
index abcee50e82..3e4ae9c68f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModPerfect : ModPerfect
{
- public override bool FailCondition(JudgementResult result)
+ public override bool ForceFail(JudgementResult result)
{
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
return false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index 56b3feb7c4..71e80e8bf7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -33,7 +33,7 @@
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
- IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IHasFailCondition
+ IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IApplicableFailOverride
{
public override string Name => "Target Practice";
public override string Acronym => "TP";
@@ -100,12 +100,12 @@ public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDr
#region Sudden Death (IApplicableFailOverride)
- public bool PerformFail() => true;
+ public bool AllowFail => true;
public bool RestartOnFail => false;
// Sudden death
- public bool FailCondition(JudgementResult result)
+ public bool ForceFail(JudgementResult result)
=> result.Type.AffectsCombo()
&& !result.IsHit;
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index be4429b283..c22a10d5af 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -440,7 +440,7 @@ public ModFailOnResult(HitResult type)
this.type = type;
}
- public override bool FailCondition(JudgementResult result) => result.Type == type;
+ public override bool ForceFail(JudgementResult result) => result.Type == type;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index a5931b98e9..247565b2a7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -61,7 +61,7 @@ private class ModFailOnFirstJudgement : ModFailCondition
public override double ScoreMultiplier => 1.0;
public override string Acronym => "";
- public override bool FailCondition(JudgementResult result) => true;
+ public override bool ForceFail(JudgementResult result) => true;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
index 8c99d739cb..59c95f8911 100644
--- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
+
namespace osu.Game.Rulesets.Mods
{
/// <summary>
@@ -11,12 +13,23 @@ public interface IApplicableFailOverride : IApplicableMod
/// <summary>
/// Whether we should allow failing at the current point in time.
/// </summary>
+ /// <remarks>
+ /// This won't trigger a failure itself, but allow other failure triggers to proceed.
+ /// The main usage is to forcefully block a failure from occurring (ie. "no fail" mods).
+ /// </remarks>
/// <returns>Whether the fail should be allowed to proceed. Return false to block.</returns>
- bool PerformFail();
+ bool AllowFail { get; }
/// <summary>
- /// Whether we want to restart on fail. Only used if <see cref="PerformFail"/> returns true.
+ /// Whether we want to restart on fail. Only used if <see cref="AllowFail"/> returns true.
/// </summary>
bool RestartOnFail { get; }
+
+ /// <summary>
+ /// Whether to force a failure based on a new judgement result.
+ /// </summary>
+ /// <param name="result">The judgement result which should be considered.</param>
+ /// <returns>Whether the fail condition has been met.</returns>
+ bool ForceFail(JudgementResult result);
}
}
diff --git a/osu.Game/Rulesets/Mods/IHasFailCondition.cs b/osu.Game/Rulesets/Mods/IHasFailCondition.cs
deleted file mode 100644
index 73c734f858..0000000000
--- a/osu.Game/Rulesets/Mods/IHasFailCondition.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mods
-{
- /// <summary>
- /// Interface for a <see cref="Mod"/> that specifies its own conditions for failure.
- /// </summary>
- // todo: maybe IHasFailCondition and IApplicableFailOverride should be combined into a single interface.
- public interface IHasFailCondition : IApplicableFailOverride
- {
- /// <summary>
- /// Determines whether <paramref name="result"/> should trigger a failure. Called every time a
- /// judgement is applied to <see cref="HealthProcessor"/>.
- /// </summary>
- /// <param name="result">The latest <see cref="JudgementResult"/>.</param>
- /// <returns>Whether the fail condition has been met.</returns>
- /// <remarks>
- /// This method should only be used to trigger failures based on <paramref name="result"/>
- /// </remarks>
- bool FailCondition(JudgementResult result);
- }
-}
diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
index 1ee0b8c466..791486f2f5 100644
--- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
+++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
@@ -74,7 +74,7 @@ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
- public override bool FailCondition(JudgementResult result) => false;
+ public override bool ForceFail(JudgementResult result) => false;
public enum AccuracyMode
{
diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs
index 0c00eb6ae0..5408034b47 100644
--- a/osu.Game/Rulesets/Mods/ModCinema.cs
+++ b/osu.Game/Rulesets/Mods/ModCinema.cs
@@ -6,6 +6,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -48,8 +49,10 @@ public void ApplyToPlayer(Player player)
player.BreakOverlay.Hide();
}
- public bool PerformFail() => false;
+ public bool AllowFail => false;
public bool RestartOnFail => false;
+
+ public bool ForceFail(JudgementResult result) => false;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
index e101ac440e..843e797381 100644
--- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
+++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
@@ -7,6 +7,7 @@
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mods
@@ -33,17 +34,21 @@ public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
retries = Retries.Value;
}
- public bool PerformFail()
+ public bool AllowFail
{
- if (retries == 0) return true;
+ get
+ {
+ if (retries == 0) return true;
- health.Value = health.MaxValue;
- retries--;
+ health.Value = health.MaxValue;
+ retries--;
- return false;
+ return false;
+ }
}
public bool RestartOnFail => false;
+ public bool ForceFail(JudgementResult result) => false;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index 8f3dec2d2b..0586986d5e 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -9,14 +9,14 @@
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IHasFailCondition
+ public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
{
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();
- public virtual bool PerformFail() => true;
+ public virtual bool AllowFail => true;
public virtual bool RestartOnFail => Restart.Value;
@@ -32,6 +32,6 @@ public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
/// </summary>
protected void TriggerFailure() => triggerFailureDelegate?.Invoke(this);
- public abstract bool FailCondition(JudgementResult result);
+ public abstract bool ForceFail(JudgementResult result);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs
index 1aaef8eac4..3a955ecde7 100644
--- a/osu.Game/Rulesets/Mods/ModNoFail.cs
+++ b/osu.Game/Rulesets/Mods/ModNoFail.cs
@@ -7,6 +7,7 @@
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
@@ -27,9 +28,10 @@ public abstract class ModNoFail : Mod, IApplicableFailOverride, IApplicableToHUD
/// <summary>
/// We never fail, 'yo.
/// </summary>
- public bool PerformFail() => false;
+ public bool AllowFail => false;
public bool RestartOnFail => false;
+ public bool ForceFail(JudgementResult result) => false;
public void ReadFromConfig(OsuConfigManager config)
{
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index d8020905da..40da4fad70 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -28,7 +28,7 @@ protected ModPerfect()
Restart.Value = Restart.Default = true;
}
- public override bool FailCondition(JudgementResult result)
+ public override bool ForceFail(JudgementResult result)
=> (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type))
&& result.Type != result.Judgement.MaxResult;
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index 070818e1c3..4e8bddf11c 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -23,7 +23,7 @@ public abstract class ModSuddenDeath : ModFailCondition
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
- public override bool FailCondition(JudgementResult result)
+ public override bool ForceFail(JudgementResult result)
=> result.Type.AffectsCombo()
&& !result.IsHit;
}
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
index aba78c4379..13cb413714 100644
--- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -102,9 +102,9 @@ private bool meetsAnyFailCondition(JudgementResult result)
if (CheckDefaultFailCondition(result))
return true;
- foreach (var condition in Mods.Value.OfType<IHasFailCondition>())
+ foreach (var condition in Mods.Value.OfType<IApplicableFailOverride>())
{
- if (condition.FailCondition(result))
+ if (condition.ForceFail(result))
{
ModTriggeringFailure = condition as Mod;
return true;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index bf9ca1c5bd..39b1dffd56 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -152,7 +152,7 @@ public abstract partial class Player : ScreenWithBeatmapBackground, ISamplePlayb
/// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing.
/// </summary>
- protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
+ protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.AllowFail);
public readonly PlayerConfiguration Configuration;
@@ -244,7 +244,7 @@ private void load(OsuConfigManager config, OsuGameBase game, CancellationToken c
HealthProcessor = gameplayMods.OfType<IApplicableHealthProcessor>().FirstOrDefault()?.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor ??= ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
- HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IHasFailCondition mod && mod.RestartOnFail).ToArray();
+ HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IApplicableFailOverride mod && mod.RestartOnFail).ToArray();
HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor);
Proposal 2: combined
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
index abcee50e82..5460ae982f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs
@@ -9,16 +9,19 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModPerfect : ModPerfect
{
- public override bool FailCondition(JudgementResult result)
+ public override FailState CheckFail(JudgementResult? result)
{
+ if (result == null)
+ return FailState.Allow;
+
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
- return false;
+ return FailState.Allow;
// Mania allows imperfect "Great" hits without failing.
if (result.Judgement.MaxResult == HitResult.Perfect)
- return result.Type < HitResult.Great;
+ return result.Type < HitResult.Great ? FailState.Allow : FailState.Force;
- return result.Type != result.Judgement.MaxResult;
+ return result.Type != result.Judgement.MaxResult ? FailState.Force : FailState.Allow;
}
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index 56b3feb7c4..5afaec4868 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -33,7 +33,7 @@
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
- IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IHasFailCondition
+ IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IApplicableFailOverride
{
public override string Name => "Target Practice";
public override string Acronym => "TP";
@@ -100,14 +100,15 @@ public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDr
#region Sudden Death (IApplicableFailOverride)
- public bool PerformFail() => true;
-
public bool RestartOnFail => false;
- // Sudden death
- public bool FailCondition(JudgementResult result)
- => result.Type.AffectsCombo()
- && !result.IsHit;
+ public FailState CheckFail(JudgementResult? result)
+ {
+ if (result == null)
+ return FailState.Allow;
+
+ return result.Type.AffectsCombo() && !result.IsHit ? FailState.Force : FailState.Allow;
+ }
#endregion
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index be4429b283..7b575183c8 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -440,7 +440,7 @@ public ModFailOnResult(HitResult type)
this.type = type;
}
- public override bool FailCondition(JudgementResult result) => result.Type == type;
+ public override FailState CheckFail(JudgementResult result) => result?.Type == type ? FailState.Force : FailState.Allow;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index a5931b98e9..964a674108 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -61,7 +61,7 @@ private class ModFailOnFirstJudgement : ModFailCondition
public override double ScoreMultiplier => 1.0;
public override string Acronym => "";
- public override bool FailCondition(JudgementResult result) => true;
+ public override FailState CheckFail(JudgementResult? result) => FailState.Force;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
index 8c99d739cb..1809e6acdd 100644
--- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
+
namespace osu.Game.Rulesets.Mods
{
/// <summary>
@@ -9,14 +11,33 @@ namespace osu.Game.Rulesets.Mods
public interface IApplicableFailOverride : IApplicableMod
{
/// <summary>
- /// Whether we should allow failing at the current point in time.
+ /// Whether we want to restart on fail.
/// </summary>
- /// <returns>Whether the fail should be allowed to proceed. Return false to block.</returns>
- bool PerformFail();
+ bool RestartOnFail { get; }
/// <summary>
- /// Whether we want to restart on fail. Only used if <see cref="PerformFail"/> returns true.
+ /// Check the current failure allowance for this mod.
/// </summary>
- bool RestartOnFail { get; }
+ /// <param name="result">The judgement result which should be considered. May be null if a failure is already being triggered.</param>
+ /// <returns>The current failure allowance (see <see cref="FailState"/>).</returns>
+ FailState CheckFail(JudgementResult? result);
+ }
+
+ public enum FailState
+ {
+ /// <summary>
+ /// Failure is being blocked by this mod.
+ /// </summary>
+ Block,
+
+ /// <summary>
+ /// Failure is allowed as per default.
+ /// </summary>
+ Allow,
+
+ /// <summary>
+ /// Failure should be forced immediately.
+ /// </summary>
+ Force
}
}
diff --git a/osu.Game/Rulesets/Mods/IHasFailCondition.cs b/osu.Game/Rulesets/Mods/IHasFailCondition.cs
deleted file mode 100644
index 73c734f858..0000000000
--- a/osu.Game/Rulesets/Mods/IHasFailCondition.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mods
-{
- /// <summary>
- /// Interface for a <see cref="Mod"/> that specifies its own conditions for failure.
- /// </summary>
- // todo: maybe IHasFailCondition and IApplicableFailOverride should be combined into a single interface.
- public interface IHasFailCondition : IApplicableFailOverride
- {
- /// <summary>
- /// Determines whether <paramref name="result"/> should trigger a failure. Called every time a
- /// judgement is applied to <see cref="HealthProcessor"/>.
- /// </summary>
- /// <param name="result">The latest <see cref="JudgementResult"/>.</param>
- /// <returns>Whether the fail condition has been met.</returns>
- /// <remarks>
- /// This method should only be used to trigger failures based on <paramref name="result"/>
- /// </remarks>
- bool FailCondition(JudgementResult result);
- }
-}
diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
index 1ee0b8c466..1176a452e0 100644
--- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
+++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
@@ -74,7 +74,7 @@ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
- public override bool FailCondition(JudgementResult result) => false;
+ public override FailState CheckFail(JudgementResult? result) => FailState.Allow;
public enum AccuracyMode
{
diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs
index 0c00eb6ae0..70a0138f8a 100644
--- a/osu.Game/Rulesets/Mods/ModCinema.cs
+++ b/osu.Game/Rulesets/Mods/ModCinema.cs
@@ -6,6 +6,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -48,8 +49,8 @@ public void ApplyToPlayer(Player player)
player.BreakOverlay.Hide();
}
- public bool PerformFail() => false;
-
public bool RestartOnFail => false;
+
+ public FailState CheckFail(JudgementResult? result) => FailState.Block;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
index e101ac440e..c22b6ad08d 100644
--- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
+++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs
@@ -7,6 +7,7 @@
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mods
@@ -33,17 +34,34 @@ public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
retries = Retries.Value;
}
- public bool PerformFail()
+ public bool AllowFail
{
- if (retries == 0) return true;
+ get
+ {
+ if (retries == 0) return true;
- health.Value = health.MaxValue;
- retries--;
+ health.Value = health.MaxValue;
+ retries--;
- return false;
+ return false;
+ }
}
public bool RestartOnFail => false;
+ public bool ForceFail(JudgementResult result) => false;
+
+ public FailState CheckFail(JudgementResult? result)
+ {
+ if (result != null)
+ return FailState.Block;
+
+ if (retries == 0) return FailState.Allow;
+
+ health.Value = health.MaxValue;
+ retries--;
+
+ return FailState.Block;
+ }
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{
diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index 8f3dec2d2b..9cb9d25be4 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -9,17 +9,16 @@
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IHasFailCondition
+ public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
{
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();
- public virtual bool PerformFail() => true;
+ public virtual bool AllowFail => true;
public virtual bool RestartOnFail => Restart.Value;
-
private Action<Mod>? triggerFailureDelegate;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@@ -32,6 +31,6 @@ public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
/// </summary>
protected void TriggerFailure() => triggerFailureDelegate?.Invoke(this);
- public abstract bool FailCondition(JudgementResult result);
+ public abstract FailState CheckFail(JudgementResult? result);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs
index 1aaef8eac4..f630ee6a3d 100644
--- a/osu.Game/Rulesets/Mods/ModNoFail.cs
+++ b/osu.Game/Rulesets/Mods/ModNoFail.cs
@@ -7,6 +7,7 @@
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
@@ -24,13 +25,10 @@ public abstract class ModNoFail : Mod, IApplicableFailOverride, IApplicableToHUD
private readonly Bindable<bool> showHealthBar = new Bindable<bool>();
- /// <summary>
- /// We never fail, 'yo.
- /// </summary>
- public bool PerformFail() => false;
-
public bool RestartOnFail => false;
+ public FailState CheckFail(JudgementResult? result) => FailState.Block;
+
public void ReadFromConfig(OsuConfigManager config)
{
config.BindWith(OsuSetting.ShowHealthDisplayWhenCantFail, showHealthBar);
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index d8020905da..a4c6192f55 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -28,9 +28,16 @@ protected ModPerfect()
Restart.Value = Restart.Default = true;
}
- public override bool FailCondition(JudgementResult result)
- => (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type))
- && result.Type != result.Judgement.MaxResult;
+ public override FailState CheckFail(JudgementResult? result)
+ {
+ if (result == null)
+ return FailState.Allow;
+
+ return (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type))
+ && result.Type != result.Judgement.MaxResult
+ ? FailState.Force
+ : FailState.Allow;
+ }
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
}
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index 070818e1c3..49b3430f24 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -23,8 +23,12 @@ public abstract class ModSuddenDeath : ModFailCondition
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
- public override bool FailCondition(JudgementResult result)
- => result.Type.AffectsCombo()
- && !result.IsHit;
+ public override FailState CheckFail(JudgementResult? result)
+ {
+ if (result == null)
+ return FailState.Allow;
+
+ return result.Type.AffectsCombo() && !result.IsHit ? FailState.Force : FailState.Allow;
+ }
}
}
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
index aba78c4379..6d8d3963e0 100644
--- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -102,9 +102,9 @@ private bool meetsAnyFailCondition(JudgementResult result)
if (CheckDefaultFailCondition(result))
return true;
- foreach (var condition in Mods.Value.OfType<IHasFailCondition>())
+ foreach (var condition in Mods.Value.OfType<IApplicableFailOverride>())
{
- if (condition.FailCondition(result))
+ if (condition.CheckFail(result) == FailState.Force)
{
ModTriggeringFailure = condition as Mod;
return true;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index bf9ca1c5bd..2f52a4df14 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -152,7 +152,7 @@ public abstract partial class Player : ScreenWithBeatmapBackground, ISamplePlayb
/// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing.
/// </summary>
- protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
+ protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.CheckFail(null) != FailState.Block);
public readonly PlayerConfiguration Configuration;
@@ -244,7 +244,7 @@ private void load(OsuConfigManager config, OsuGameBase game, CancellationToken c
HealthProcessor = gameplayMods.OfType<IApplicableHealthProcessor>().FirstOrDefault()?.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor ??= ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
- HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IHasFailCondition mod && mod.RestartOnFail).ToArray();
+ HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IApplicableFailOverride mod && mod.RestartOnFail).ToArray();
HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor);
cc @smoogipoo @frenzibyte
My opinion might be biased because I've already gone through something similar to this (see https://github.com/ppy/osu/compare/master...frenzibyte:osu:refactor-fail-and-fix-issue), but I prefer the second proposal from a code-quality perspective because both blocking & triggering fail is decided by a single point in HealthProcessor (which is the meetsAnyFailCondition method).
I meant to open a PR of the above but there were test failures and I couldn't get to it in time.
From the look of it I definitely prefer the second proposal.
I'll tidy up the second proposal and commit it to this branch.
Sorry for the delay, lost track of this one. I've applied the diff with multiple fixes and improvements.
Will require a re-review from someone else with fresh eyes.