From de4d8eb1965a82d5d59c8bc0d87476cdec03d539 Mon Sep 17 00:00:00 2001 From: Brayzure Date: Sat, 18 Nov 2017 01:28:09 -0500 Subject: [PATCH 1/9] Implement Sudden Death and Perfect - Two additional fail conditions --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 23 ++++++++++++++++++--- osu.Game/Screens/Play/Player.cs | 6 ++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4dd88600b2..f579b94c69 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -66,6 +66,8 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool HasCompleted => false; + public int strictFail = 0; + /// /// Whether this ScoreProcessor has already triggered the failed state. /// @@ -76,6 +78,16 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool FailCondition => Health.Value == Health.MinValue; + /// + /// The conditions for failing if the Sudden Death mod is enabled. + /// + protected virtual bool SuddenDeathFailCondition => Combo.Value != HighestCombo.Value; + + /// + /// The conditions for failing if the Perfect mod is enabled. + /// + protected virtual bool PerfectFailCondition => Accuracy.Value != 1; + protected ScoreProcessor() { Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; @@ -121,11 +133,16 @@ namespace osu.Game.Rulesets.Scoring /// protected void UpdateFailed() { - if (HasFailed || !FailCondition) + if (HasFailed) return; - if (Failed?.Invoke() != false) - HasFailed = true; + if(FailCondition || + (strictFail==1 && SuddenDeathFailCondition) || + (strictFail==2 && PerfectFailCondition)) + { + if (Failed?.Invoke() != false) + HasFailed = true; + } } /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3e57e18963..2603fee769 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -227,6 +227,12 @@ namespace osu.Game.Screens.Play // Bind ScoreProcessor to ourselves scoreProcessor.AllJudged += onCompletion; scoreProcessor.Failed += onFail; + + if (Beatmap.Value.Mods.Value.Any(m => m.Name == "Sudden Death")) + scoreProcessor.strictFail = 1; + + if (Beatmap.Value.Mods.Value.Any(m => m.Name == "Perfect")) + scoreProcessor.strictFail = 2; } private void applyRateFromMods() From da30d76f9b08b1435db366b5b0004cb704fd5f94 Mon Sep 17 00:00:00 2001 From: Brayzure Date: Mon, 20 Nov 2017 02:15:29 -0500 Subject: [PATCH 2/9] Implement Score Processor Mod Interface - Add a delegate whenever we want to register an additional fail condition --- .../Mods/IApplicableToScoreProcessor.cs | 15 +++++++ osu.Game/Rulesets/Mods/ModPerfect.cs | 16 ++++++- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 15 ++++++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 43 ++++++++++--------- osu.Game/Screens/Play/Player.cs | 12 ++++-- osu.Game/osu.Game.csproj | 1 + 6 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs new file mode 100644 index 0000000000..db9b713c59 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for mods that make general adjustments to score processor. + /// + public interface IApplicableToScoreProcessor + { + void ApplyToScoreProcessor(ScoreProcessor scoreProcessor); + } +} diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 082370ea5d..7b79493d65 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -1,12 +1,24 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets.Mods { - public abstract class ModPerfect : ModSuddenDeath + public abstract class ModPerfect : ModSuddenDeath, IApplicableToScoreProcessor { public override string Name => "Perfect"; public override string ShortenedName => "PF"; public override string Description => "SS or quit."; + + public bool onFailCheck(ScoreProcessor scoreProcessor) + { + return scoreProcessor.Accuracy.Value != 1; + } + + public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + scoreProcessor.FailChecker += onFailCheck; + } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 999cb40f89..c71442fa28 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -3,10 +3,11 @@ using System; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod + public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor { public override string Name => "Sudden Death"; public override string ShortenedName => "SD"; @@ -16,5 +17,15 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + + public bool onFailCheck(ScoreProcessor scoreProcessor) + { + return scoreProcessor.Combo.Value != scoreProcessor.HighestCombo.Value; + } + + public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + scoreProcessor.FailChecker += onFailCheck; + } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f579b94c69..997dfc2a58 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Collections.Generic; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -31,6 +32,11 @@ namespace osu.Game.Rulesets.Scoring /// public event Action NewJudgement; + /// + /// Invoked when we want to check if a failure condition has been fulfilled + /// + public event Func FailChecker; + /// /// The current total score. /// @@ -66,8 +72,6 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool HasCompleted => false; - public int strictFail = 0; - /// /// Whether this ScoreProcessor has already triggered the failed state. /// @@ -78,16 +82,6 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool FailCondition => Health.Value == Health.MinValue; - /// - /// The conditions for failing if the Sudden Death mod is enabled. - /// - protected virtual bool SuddenDeathFailCondition => Combo.Value != HighestCombo.Value; - - /// - /// The conditions for failing if the Perfect mod is enabled. - /// - protected virtual bool PerfectFailCondition => Accuracy.Value != 1; - protected ScoreProcessor() { Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; @@ -133,16 +127,11 @@ namespace osu.Game.Rulesets.Scoring /// protected void UpdateFailed() { - if (HasFailed) + if (HasFailed || !FailCondition) return; - if(FailCondition || - (strictFail==1 && SuddenDeathFailCondition) || - (strictFail==2 && PerfectFailCondition)) - { - if (Failed?.Invoke() != false) - HasFailed = true; - } + if (Failed?.Invoke() != false) + HasFailed = true; } /// @@ -157,6 +146,18 @@ namespace osu.Game.Rulesets.Scoring AllJudged?.Invoke(); } + protected void CheckAlternateFailConditions() + { + if (HasFailed) + return; + + if (FailChecker?.Invoke(this) == true) + { + if (Failed?.Invoke() != false) + HasFailed = true; + } + } + /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// @@ -233,6 +234,8 @@ namespace osu.Game.Rulesets.Scoring OnNewJudgement(judgement); updateScore(); + CheckAlternateFailConditions(); + NotifyNewJudgement(judgement); UpdateFailed(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2603fee769..0bff13a46a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -228,11 +228,15 @@ namespace osu.Game.Screens.Play scoreProcessor.AllJudged += onCompletion; scoreProcessor.Failed += onFail; - if (Beatmap.Value.Mods.Value.Any(m => m.Name == "Sudden Death")) - scoreProcessor.strictFail = 1; + applyAlternateFailConditions(); + } - if (Beatmap.Value.Mods.Value.Any(m => m.Name == "Perfect")) - scoreProcessor.strictFail = 2; + private void applyAlternateFailConditions() + { + foreach(var mod in Beatmap.Value.Mods.Value.OfType()) + { + mod.ApplyToScoreProcessor(scoreProcessor); + } } private void applyRateFromMods() diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7b479bdba2..5306f0df4a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -294,6 +294,7 @@ + From 6d74fd254c389d71fe2af0c712e3307f2ad92518 Mon Sep 17 00:00:00 2001 From: Brayzure Date: Mon, 20 Nov 2017 20:25:14 -0500 Subject: [PATCH 3/9] Remove Extraneous Using Statement - System.Collections.Generic was leftover from a previous attempt --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 997dfc2a58..c3ae887a9f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Collections.Generic; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; From 2b0295ed86660cd9729abfe616e3ad6a24e282c8 Mon Sep 17 00:00:00 2001 From: Brayzure Date: Mon, 20 Nov 2017 20:49:31 -0500 Subject: [PATCH 4/9] Proper Public Method Case - onFailCheck to OnFailCheck --- osu.Game/Rulesets/Mods/ModPerfect.cs | 4 ++-- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 7b79493d65..cf7bf141c3 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "PF"; public override string Description => "SS or quit."; - public bool onFailCheck(ScoreProcessor scoreProcessor) + public bool OnFailCheck(ScoreProcessor scoreProcessor) { return scoreProcessor.Accuracy.Value != 1; } public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - scoreProcessor.FailChecker += onFailCheck; + scoreProcessor.FailChecker += OnFailCheck; } } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index c71442fa28..6675c41bd4 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool onFailCheck(ScoreProcessor scoreProcessor) + public bool OnFailCheck(ScoreProcessor scoreProcessor) { return scoreProcessor.Combo.Value != scoreProcessor.HighestCombo.Value; } public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - scoreProcessor.FailChecker += onFailCheck; + scoreProcessor.FailChecker += OnFailCheck; } } } From 1b27ce6198eaa525deb2560ee75f76d7e3d9570d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Nov 2017 17:09:22 +0900 Subject: [PATCH 5/9] Cleanup + renaming --- .../Scoring/TaikoScoreProcessor.cs | 2 +- osu.Game/Rulesets/Mods/ModPerfect.cs | 10 +--------- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 9 +++------ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 ++++++------ 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 0e5df329d8..0048566b15 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring /// /// Taiko fails at the end of the map if the player has not half-filled their HP bar. /// - protected override bool FailCondition => Hits == MaxHits && Health.Value <= 0.5; + protected override bool DefaultFailCondition => Hits == MaxHits && Health.Value <= 0.5; private double hpIncreaseTick; private double hpIncreaseGreat; diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index cf7bf141c3..e7887c8fc4 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -11,14 +11,6 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "PF"; public override string Description => "SS or quit."; - public bool OnFailCheck(ScoreProcessor scoreProcessor) - { - return scoreProcessor.Accuracy.Value != 1; - } - - public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) - { - scoreProcessor.FailChecker += OnFailCheck; - } + protected override bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Accuracy.Value != 1; } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 6675c41bd4..17350fdaa4 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -18,14 +18,11 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool OnFailCheck(ScoreProcessor scoreProcessor) + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - return scoreProcessor.Combo.Value != scoreProcessor.HighestCombo.Value; + scoreProcessor.FailConditions += FailCondition; } - public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) - { - scoreProcessor.FailChecker += OnFailCheck; - } + protected virtual bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Combo.Value != scoreProcessor.HighestCombo.Value; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index c3ae887a9f..de8a510590 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Scoring public event Action NewJudgement; /// - /// Invoked when we want to check if a failure condition has been fulfilled + /// Additional conditions on top of that cause a failing state. /// - public event Func FailChecker; + public event Func FailConditions; /// /// The current total score. @@ -77,9 +77,9 @@ namespace osu.Game.Rulesets.Scoring public virtual bool HasFailed { get; private set; } /// - /// The conditions for failing. + /// The default conditions for failing. /// - protected virtual bool FailCondition => Health.Value == Health.MinValue; + protected virtual bool DefaultFailCondition => Health.Value == Health.MinValue; protected ScoreProcessor() { @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Scoring /// protected void UpdateFailed() { - if (HasFailed || !FailCondition) + if (HasFailed || !DefaultFailCondition) return; if (Failed?.Invoke() != false) @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Scoring if (HasFailed) return; - if (FailChecker?.Invoke(this) == true) + if (FailConditions?.Invoke(this) == true) { if (Failed?.Invoke() != false) HasFailed = true; From 18b9828c49548192048b84b74fcd35b515823c53 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Nov 2017 17:11:07 +0900 Subject: [PATCH 6/9] Merge UpdateFailed and CheckAlternateFailConditions --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index de8a510590..7b26e50dd8 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -126,7 +126,10 @@ namespace osu.Game.Rulesets.Scoring /// protected void UpdateFailed() { - if (HasFailed || !DefaultFailCondition) + if (HasFailed) + return; + + if (!DefaultFailCondition && FailConditions?.Invoke(this) != true) return; if (Failed?.Invoke() != false) @@ -145,18 +148,6 @@ namespace osu.Game.Rulesets.Scoring AllJudged?.Invoke(); } - protected void CheckAlternateFailConditions() - { - if (HasFailed) - return; - - if (FailConditions?.Invoke(this) == true) - { - if (Failed?.Invoke() != false) - HasFailed = true; - } - } - /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// @@ -233,8 +224,6 @@ namespace osu.Game.Rulesets.Scoring OnNewJudgement(judgement); updateScore(); - CheckAlternateFailConditions(); - NotifyNewJudgement(judgement); UpdateFailed(); } From fea56322f0aadcdb1b48a82f4836a47d268cd213 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Nov 2017 17:28:51 +0900 Subject: [PATCH 7/9] Fix SD not failing for the first note --- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 17350fdaa4..bc42c69cbe 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -23,6 +23,6 @@ namespace osu.Game.Rulesets.Mods scoreProcessor.FailConditions += FailCondition; } - protected virtual bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Combo.Value != scoreProcessor.HighestCombo.Value; + protected virtual bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Combo.Value == 0; } } From c30d31e03746efd290ca09ae82cd0701a3ce2e33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Nov 2017 17:30:19 +0900 Subject: [PATCH 8/9] Remove extra alternateFailConditions function in Player --- osu.Game/Screens/Play/Player.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0bff13a46a..cd2818398d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -228,15 +228,8 @@ namespace osu.Game.Screens.Play scoreProcessor.AllJudged += onCompletion; scoreProcessor.Failed += onFail; - applyAlternateFailConditions(); - } - - private void applyAlternateFailConditions() - { - foreach(var mod in Beatmap.Value.Mods.Value.OfType()) - { - mod.ApplyToScoreProcessor(scoreProcessor); - } + foreach (var mod in Beatmap.Value.Mods.Value.OfType()) + mod.ApplyToScoreProcessor(scoreProcessor); } private void applyRateFromMods() From 217554f587d40582ce51b2c5630c8a0e4402791d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Nov 2017 18:06:24 +0900 Subject: [PATCH 9/9] Remove redundant interface --- osu.Game/Rulesets/Mods/ModPerfect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index e7887c8fc4..59539d2b2c 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModPerfect : ModSuddenDeath, IApplicableToScoreProcessor + public abstract class ModPerfect : ModSuddenDeath { public override string Name => "Perfect"; public override string ShortenedName => "PF";