From 0e52858d364f5e8a53f1311ccdc679b8c31985fb Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Fri, 23 May 2025 01:07:42 +0100 Subject: [PATCH 1/9] add mines --- Quaver.API/Enums/HitObjectType.cs | 10 +++ .../Processors/Scoring/ScoreProcessorKeys.cs | 13 +-- Quaver.API/Maps/Qua.cs | 12 ++- Quaver.API/Maps/Structures/HitObjectInfo.cs | 12 +++ Quaver.API/Replays/Replay.cs | 3 + .../Replays/Virtual/VirtualReplayPlayer.cs | 81 ++++++++++++++++++- 6 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 Quaver.API/Enums/HitObjectType.cs diff --git a/Quaver.API/Enums/HitObjectType.cs b/Quaver.API/Enums/HitObjectType.cs new file mode 100644 index 000000000..8455b270c --- /dev/null +++ b/Quaver.API/Enums/HitObjectType.cs @@ -0,0 +1,10 @@ +namespace Quaver.API.Enums +{ + /// + /// + public enum HitObjectType + { + Normal, + Mine + } +} \ No newline at end of file diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 2ef72870d..48da3f9af 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -14,6 +14,7 @@ using Quaver.API.Maps.Processors.Scoring.Data; using Quaver.API.Maps.Processors.Scoring.Multiplayer; using Quaver.API.Replays; +using HitObjectType = Quaver.API.Enums.HitObjectType; namespace Quaver.API.Maps.Processors.Scoring { @@ -364,17 +365,7 @@ protected override void InitializeHealthWeighting() /// public int GetTotalJudgementCount() { - var judgements = 0; - - foreach (var o in Map.HitObjects) - { - if (o.IsLongNote) - judgements += 2; - else - judgements++; - } - - return judgements; + return Map.HitObjects.Sum(o => o.JudgementCount); } /// diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index e6e955fae..94141e6ff 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -359,7 +359,8 @@ static HitObjectInfo SerializableHitObject(HitObjectInfo obj) => .Select(x => new KeySoundInfo { Sample = x.Sample, Volume = x.Volume == 100 ? 0 : x.Volume }) .ToList(), Lane = obj.Lane, StartTime = obj.StartTime, - TimingGroup = obj.TimingGroup == DefaultScrollGroupId ? null : obj.TimingGroup + TimingGroup = obj.TimingGroup == DefaultScrollGroupId ? null : obj.TimingGroup, + Type = obj.Type }; static SoundEffectInfo SerializableSoundEffect(SoundEffectInfo x) => @@ -1110,8 +1111,15 @@ public HitObjectInfo GetHitObjectAtJudgementIndex(int index) // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (var h in HitObjects) - if (total++ == index || (h.IsLongNote && total++ == index)) + { + var judgementCount = h.JudgementCount; + if (total <= index && index < total + judgementCount) + { return h; + } + + total += judgementCount; + } return null; } diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 6aa26f73c..197e32ddf 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -64,6 +64,11 @@ public HitSounds HitSound set; } + /// + /// The hit object could be a normal note or a mine + /// + public HitObjectType Type { get; [MoonSharpVisible(false)] set; } + /// /// Key sounds to play when this object is hit. /// @@ -95,6 +100,11 @@ public string TimingGroup [YamlIgnore] public bool IsLongNote => EndTime > 0; + /// + /// The number of judgements generated by this object + /// + [YamlIgnore] public int JudgementCount => IsLongNote && Type != HitObjectType.Mine ? 2 : 1; + /// /// Returns if the object is allowed to be edited in lua scripts /// @@ -175,6 +185,7 @@ public bool Equals(HitObjectInfo x, HitObjectInfo y) x.Lane == y.Lane && x.EndTime == y.EndTime && x.HitSound == y.HitSound && + x.Type == y.Type && x.KeySounds.SequenceEqual(y.KeySounds, KeySoundInfo.ByValueComparer) && x.EditorLayer == y.EditorLayer; } @@ -186,6 +197,7 @@ public int GetHashCode(HitObjectInfo obj) var hashCode = obj.StartTime; hashCode = (hashCode * 397) ^ obj.Lane; hashCode = (hashCode * 397) ^ obj.EndTime; + hashCode = (hashCode * 397) ^ (int)obj.Type; hashCode = (hashCode * 397) ^ (int)obj.HitSound; foreach (var keySound in obj.KeySounds) diff --git a/Quaver.API/Replays/Replay.cs b/Quaver.API/Replays/Replay.cs index d0af0977c..2424258ff 100644 --- a/Quaver.API/Replays/Replay.cs +++ b/Quaver.API/Replays/Replay.cs @@ -332,6 +332,9 @@ public static Replay GeneratePerfectReplayKeys(Replay replay, Qua map) foreach (var hitObject in map.HitObjects) { + if (hitObject.Type is HitObjectType.Mine) + continue; + // Add key press frame nonCombined.Add(new ReplayAutoplayFrame(hitObject, ReplayAutoplayFrameType.Press, hitObject.StartTime, KeyLaneToPressState(hitObject.Lane))); diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index ee055a6d9..a287f9e53 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -32,6 +32,18 @@ public class VirtualReplayPlayer /// The score processor for the virtual replay. /// public ScoreProcessorKeys ScoreProcessor { get; } + + + /// + /// All of the mines that are currently active and available. + /// + public List ActiveMines { get; } + + + /// + /// The list of active mines that are scheduled for removal. + /// + public List ActiveMinesToRemove { get; set; } /// /// All of the HitObjects that are currently active and available. @@ -95,8 +107,22 @@ public VirtualReplayPlayer(Replay replay, Qua map, JudgementWindows windows = nu ActiveHitObjects = new List(); ActiveHeldLongNotes = new List(); + ActiveMines = new List(); - map.HitObjects.ForEach(x => ActiveHitObjects.Add(x)); + map.HitObjects.ForEach(x => + { + switch (x.Type) + { + case HitObjectType.Normal: + ActiveHitObjects.Add(x); + break; + case HitObjectType.Mine: + ActiveMines.Add(x); + break; + default: + throw new ArgumentOutOfRangeException(); + } + }); // Add virtual key bindings based on the game mode of the replay. switch (Map.Mode) @@ -171,6 +197,7 @@ public void PlayNextFrame() // Store the objects that need to be removed from the list of active objects. ActiveHitObjectsToRemove = new List(); ActiveHeldLongNotesToRemove = new List(); + ActiveMinesToRemove = new List(); if (CurrentFrame < Replay.Frames.Count) { @@ -207,6 +234,8 @@ private void HandleKeyPressesInFrame() // Retrieve a list of the key press states in integer form. var currentFramePressed = Replay.KeyPressStateToLanes(Replay.Frames[CurrentFrame].Keys); var previousFramePressed = CurrentFrame > 0 ? Replay.KeyPressStateToLanes(Replay.Frames[CurrentFrame - 1].Keys) : new List(); + + var previousFrameTime = CurrentFrame > 0 ? Replay.Frames[CurrentFrame - 1].Time : Time; // Update the key press state in the store. for (var i = 0; i < InputKeyStore.Count; i++) @@ -217,6 +246,33 @@ private void HandleKeyPressesInFrame() .Concat(previousFramePressed.Except(currentFramePressed)) .ToList(); + foreach (var lane in previousFramePressed) + { + foreach (var mine in ActiveMines) + { + var endTime = mine.IsLongNote ? mine.EndTime : mine.StartTime; + if (mine.Lane == lane + 1 + && endTime + ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime + && Time >= mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv]) + { + // Calculate the hit difference. + var hitDifference = + mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime + ? (int)ScoreProcessor.JudgementWindow[Judgement.Marv] + : mine.StartTime - previousFrameTime; + + // Add a new hit stat to the score processor. + var stat = new HitStat(HitStatType.Miss, KeyPressType.Press, mine, Time, Judgement.Miss, hitDifference, + ScoreProcessor.Accuracy, ScoreProcessor.Health); + + ScoreProcessor.Stats.Add(stat); + + // Object needs to be removed from ActiveObjects. + ActiveMinesToRemove.Add(mine); + } + } + } + // Go through each frame and handle key presses/releases. foreach (var key in keyDifferences) { @@ -323,6 +379,7 @@ private void HandleKeyPressesInFrame() // Remove all active objects after handling key presses/releases. ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x)); ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x)); + ActiveMinesToRemove.ForEach(x => ActiveMines.Remove(x)); } /// @@ -390,10 +447,32 @@ private void HandleMissedHitObjects() break; } } + // Handle missed mines. + foreach (var hitObject in ActiveMines) + { + var endTime = hitObject.IsLongNote ? hitObject.EndTime : hitObject.StartTime; + if (Time > endTime + ScoreProcessor.JudgementWindow[Judgement.Marv]) + { + // Add a miss to the score. + ScoreProcessor.CalculateScore(Judgement.Marv); + + // Create a new HitStat to add to the ScoreProcessor. + var stat = new HitStat(HitStatType.Hit, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Marv, 0, + ScoreProcessor.Accuracy, ScoreProcessor.Health); + + ScoreProcessor.Stats.Add(stat); + ActiveMinesToRemove.Add(hitObject); + } + else if (Time < hitObject.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv]) + { + break; + } + } // Remove all objects ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x)); ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x)); + ActiveMinesToRemove.ForEach(x => ActiveMines.Remove(x)); } /// From d65ee56da6a599df448579e7158117f71d96a253 Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Mon, 26 May 2025 21:35:14 +0100 Subject: [PATCH 2/9] make ScoreProcessor.CalculateScore() more consistent --- .../Maps/Processors/Scoring/ScoreProcessor.cs | 2 +- .../Processors/Scoring/ScoreProcessorKeys.cs | 16 +++++++++++++--- .../Replays/Virtual/VirtualReplayPlayer.cs | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs index 70b866c6e..a07dc1db2 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null) /// /// Adds a judgement to the score and recalculates the score. /// - public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false); + public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false); /// /// Calculates the accuracy of the current play session. diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 48da3f9af..598b3a3a5 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -173,8 +173,9 @@ public ScoreProcessorKeys(Replay replay, JudgementWindows windows = null) : base /// /// /// + /// /// - public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true) + public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool isMine, bool calculateAllStats = true) { var absoluteDifference = 0; @@ -220,13 +221,20 @@ public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bo return judgement; } + public void CalculateScore(HitStat hitStat) + { + CalculateScore(hitStat.Judgement, hitStat.KeyPressType == KeyPressType.Release, + hitStat.HitObject.Type is HitObjectType.Mine); + } + /// /// /// Calculate Score and Health increase/decrease with a given judgement. /// /// /// - public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false) + /// + public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false) { // Update Judgement count CurrentJudgements[judgement]++; @@ -250,7 +258,9 @@ public override void CalculateScore(Judgement judgement, bool isLongNoteRelease MultiplierCount++; // Add to the combo since the user hit. - Combo++; + // Only do this when the note is not a mine (so it is a regular note) + if (!isMine) + Combo++; // Set the max combo if applicable. if (Combo > MaxCombo) diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index a287f9e53..a83e2d1c9 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -298,7 +298,7 @@ private void HandleKeyPressesInFrame() var hitDifference = hitObject.StartTime - Time; // Calculate Score. - var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press); + var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press, false); switch (judgement) { @@ -347,7 +347,7 @@ private void HandleKeyPressesInFrame() var hitDifference = hitObject.EndTime - Time; // Calculate Score - var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release); + var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release, false); // LN was released during a hit window. if (judgement != Judgement.Ghost && judgement != Judgement.Miss) From 380bd77a4f074235d31415a6b075836b5f7b9bdd Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Mon, 9 Jun 2025 22:32:47 +0100 Subject: [PATCH 3/9] Require isLnRelease and isMine parameter to be explicitly given in CalculateScore(); --- .../Maps/Processors/Scoring/ScoreProcessor.cs | 2 +- .../Processors/Scoring/ScoreProcessorKeys.cs | 4 +-- .../Replays/Virtual/VirtualReplayPlayer.cs | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs index a07dc1db2..e6e7e2d0e 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null) /// /// Adds a judgement to the score and recalculates the score. /// - public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false); + public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine); /// /// Calculates the accuracy of the current play session. diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 598b3a3a5..9957a33c2 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -216,7 +216,7 @@ public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bo return judgement; if (calculateAllStats) - CalculateScore(judgement, keyPressType == KeyPressType.Release); + CalculateScore(judgement, keyPressType == KeyPressType.Release, isMine); return judgement; } @@ -234,7 +234,7 @@ public void CalculateScore(HitStat hitStat) /// /// /// - public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false) + public override void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine) { // Update Judgement count CurrentJudgements[judgement]++; diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index a83e2d1c9..8806b772e 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -174,10 +174,12 @@ public void PlayNextFrame() { var obj = Map.GetHitObjectAtJudgementIndex(i); - ScoreProcessor.CalculateScore(Judgement.Miss); + var hitStat = new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime, + Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health); - ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime, - Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); + ScoreProcessor.CalculateScore(hitStat); + + ScoreProcessor.Stats.Add(hitStat); if (!ScoreProcessor.Failed) continue; @@ -310,7 +312,7 @@ private void HandleKeyPressesInFrame() // Add another miss for an LN (head and tail) if (hitObject.IsLongNote) { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.Press, hitObject, Time, Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); @@ -361,7 +363,7 @@ private void HandleKeyPressesInFrame() // The LN was released too early (miss) else { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); // Add a new stat to ScoreProcessor. var stat = new HitStat(HitStatType.Hit, KeyPressType.Release, hitObject, Time, Judgement.Miss, hitDifference, @@ -399,7 +401,7 @@ private void HandleMissedLongNoteReleases() // Judgement when a user doesn't release an LN. var missedReleaseJudgement = Judgement.Good; - ScoreProcessor.CalculateScore(missedReleaseJudgement, true); + ScoreProcessor.CalculateScore(missedReleaseJudgement, true, false); // Add new miss stat. var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.EndTime, missedReleaseJudgement, int.MinValue, @@ -424,19 +426,20 @@ private void HandleMissedHitObjects() { if (Time > hitObject.StartTime + ScoreProcessor.JudgementWindow[Judgement.Okay]) { - // Add a miss to the score. - ScoreProcessor.CalculateScore(Judgement.Miss); - // Create a new HitStat to add to the ScoreProcessor. var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health); + // Add a miss to the score. + ScoreProcessor.CalculateScore(stat); + + ScoreProcessor.Stats.Add(stat); // Long notes count as two misses, so add another one if the object is one. if (hitObject.IsLongNote) { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); ScoreProcessor.Stats.Add(stat); } @@ -453,13 +456,13 @@ private void HandleMissedHitObjects() var endTime = hitObject.IsLongNote ? hitObject.EndTime : hitObject.StartTime; if (Time > endTime + ScoreProcessor.JudgementWindow[Judgement.Marv]) { - // Add a miss to the score. - ScoreProcessor.CalculateScore(Judgement.Marv); - // Create a new HitStat to add to the ScoreProcessor. var stat = new HitStat(HitStatType.Hit, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Marv, 0, ScoreProcessor.Accuracy, ScoreProcessor.Health); + // Add a miss to the score. + ScoreProcessor.CalculateScore(stat); + ScoreProcessor.Stats.Add(stat); ActiveMinesToRemove.Add(hitObject); } From 21bdb36290d570f98aba5acf7fc12707635326d3 Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Mon, 9 Jun 2025 22:32:47 +0100 Subject: [PATCH 4/9] Require isLnRelease and isMine parameter to be explicitly given in CalculateScore(); --- .../Maps/Processors/Scoring/ScoreProcessor.cs | 2 +- .../Processors/Scoring/ScoreProcessorKeys.cs | 4 +-- .../Replays/Virtual/VirtualReplayPlayer.cs | 29 ++++++++++--------- Quaver.Tools/Commands/RecalculateCommand.cs | 12 ++++---- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs index a07dc1db2..e6e7e2d0e 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null) /// /// Adds a judgement to the score and recalculates the score. /// - public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false); + public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine); /// /// Calculates the accuracy of the current play session. diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 598b3a3a5..9957a33c2 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -216,7 +216,7 @@ public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bo return judgement; if (calculateAllStats) - CalculateScore(judgement, keyPressType == KeyPressType.Release); + CalculateScore(judgement, keyPressType == KeyPressType.Release, isMine); return judgement; } @@ -234,7 +234,7 @@ public void CalculateScore(HitStat hitStat) /// /// /// - public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false) + public override void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine) { // Update Judgement count CurrentJudgements[judgement]++; diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index a83e2d1c9..8806b772e 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -174,10 +174,12 @@ public void PlayNextFrame() { var obj = Map.GetHitObjectAtJudgementIndex(i); - ScoreProcessor.CalculateScore(Judgement.Miss); + var hitStat = new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime, + Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health); - ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime, - Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); + ScoreProcessor.CalculateScore(hitStat); + + ScoreProcessor.Stats.Add(hitStat); if (!ScoreProcessor.Failed) continue; @@ -310,7 +312,7 @@ private void HandleKeyPressesInFrame() // Add another miss for an LN (head and tail) if (hitObject.IsLongNote) { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.Press, hitObject, Time, Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); @@ -361,7 +363,7 @@ private void HandleKeyPressesInFrame() // The LN was released too early (miss) else { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); // Add a new stat to ScoreProcessor. var stat = new HitStat(HitStatType.Hit, KeyPressType.Release, hitObject, Time, Judgement.Miss, hitDifference, @@ -399,7 +401,7 @@ private void HandleMissedLongNoteReleases() // Judgement when a user doesn't release an LN. var missedReleaseJudgement = Judgement.Good; - ScoreProcessor.CalculateScore(missedReleaseJudgement, true); + ScoreProcessor.CalculateScore(missedReleaseJudgement, true, false); // Add new miss stat. var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.EndTime, missedReleaseJudgement, int.MinValue, @@ -424,19 +426,20 @@ private void HandleMissedHitObjects() { if (Time > hitObject.StartTime + ScoreProcessor.JudgementWindow[Judgement.Okay]) { - // Add a miss to the score. - ScoreProcessor.CalculateScore(Judgement.Miss); - // Create a new HitStat to add to the ScoreProcessor. var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health); + // Add a miss to the score. + ScoreProcessor.CalculateScore(stat); + + ScoreProcessor.Stats.Add(stat); // Long notes count as two misses, so add another one if the object is one. if (hitObject.IsLongNote) { - ScoreProcessor.CalculateScore(Judgement.Miss, true); + ScoreProcessor.CalculateScore(Judgement.Miss, true, false); ScoreProcessor.Stats.Add(stat); } @@ -453,13 +456,13 @@ private void HandleMissedHitObjects() var endTime = hitObject.IsLongNote ? hitObject.EndTime : hitObject.StartTime; if (Time > endTime + ScoreProcessor.JudgementWindow[Judgement.Marv]) { - // Add a miss to the score. - ScoreProcessor.CalculateScore(Judgement.Marv); - // Create a new HitStat to add to the ScoreProcessor. var stat = new HitStat(HitStatType.Hit, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Marv, 0, ScoreProcessor.Accuracy, ScoreProcessor.Health); + // Add a miss to the score. + ScoreProcessor.CalculateScore(stat); + ScoreProcessor.Stats.Add(stat); ActiveMinesToRemove.Add(hitObject); } diff --git a/Quaver.Tools/Commands/RecalculateCommand.cs b/Quaver.Tools/Commands/RecalculateCommand.cs index f27d110d9..9379fef22 100644 --- a/Quaver.Tools/Commands/RecalculateCommand.cs +++ b/Quaver.Tools/Commands/RecalculateCommand.cs @@ -63,22 +63,22 @@ private void Recalculate(JToken scores) var processor = new ScoreProcessorKeys(qua, 0); for (var i = 0; i < (int)score["count_marv"]; i++) - processor.CalculateScore(Judgement.Marv); + processor.CalculateScore(Judgement.Marv, false, false); for (var i = 0; i < (int)score["count_perf"]; i++) - processor.CalculateScore(Judgement.Perf); + processor.CalculateScore(Judgement.Perf, false, false); for (var i = 0; i < (int)score["count_great"]; i++) - processor.CalculateScore(Judgement.Great); + processor.CalculateScore(Judgement.Great, false, false); for (var i = 0; i < (int)score["count_good"]; i++) - processor.CalculateScore(Judgement.Good); + processor.CalculateScore(Judgement.Good, false, false); for (var i = 0; i < (int)score["count_okay"]; i++) - processor.CalculateScore(Judgement.Okay); + processor.CalculateScore(Judgement.Okay, false, false); for (var i = 0; i < (int)score["count_miss"]; i++) - processor.CalculateScore(Judgement.Miss); + processor.CalculateScore(Judgement.Miss, false, false); var difficultyRating = (double)score["performance_rating"] / Math.Pow((double)score["accuracy"] / 98, 6); From 9c92abf19c8d4cf135274fbeac241c5028c44914 Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Mon, 9 Jun 2025 22:57:39 +0100 Subject: [PATCH 5/9] Add documentation for HitObjectType.cs --- Quaver.API/Enums/HitObjectType.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Quaver.API/Enums/HitObjectType.cs b/Quaver.API/Enums/HitObjectType.cs index 8455b270c..0e9a48027 100644 --- a/Quaver.API/Enums/HitObjectType.cs +++ b/Quaver.API/Enums/HitObjectType.cs @@ -1,10 +1,11 @@ namespace Quaver.API.Enums { /// + /// Indicates the type of a hit object /// public enum HitObjectType { - Normal, - Mine + Normal, // Regular hit object. It should be hit normally. + Mine // A mine object. It should not be hit, and hitting it will result in a miss. } } \ No newline at end of file From a6ac5c2606a9631ad9c2770e75a1a456a845eb86 Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Tue, 10 Jun 2025 01:46:34 +0100 Subject: [PATCH 6/9] Make CalculateScore() arguments optional again since we might not be merging shared soon --- Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs | 2 +- Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs index e6e7e2d0e..a07dc1db2 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null) /// /// Adds a judgement to the score and recalculates the score. /// - public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine); + public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false); /// /// Calculates the accuracy of the current play session. diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 9957a33c2..b83a05469 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -234,7 +234,7 @@ public void CalculateScore(HitStat hitStat) /// /// /// - public override void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine) + public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false) { // Update Judgement count CurrentJudgements[judgement]++; From 7a658886797d07beeef1a19c0d8d1ca914cca80b Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Mon, 23 Jun 2025 14:37:59 +0100 Subject: [PATCH 7/9] Fix method ordering to fit develop branch --- Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs | 2 +- Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs | 7 ++++--- Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs index e6e7e2d0e..a07dc1db2 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null) /// /// Adds a judgement to the score and recalculates the score. /// - public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine); + public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false); /// /// Calculates the accuracy of the current play session. diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs index 9957a33c2..94df2a349 100644 --- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs @@ -173,9 +173,10 @@ public ScoreProcessorKeys(Replay replay, JudgementWindows windows = null) : base /// /// /// - /// /// - public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool isMine, bool calculateAllStats = true) + /// + public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true, + bool isMine = false) { var absoluteDifference = 0; @@ -234,7 +235,7 @@ public void CalculateScore(HitStat hitStat) /// /// /// - public override void CalculateScore(Judgement judgement, bool isLongNoteRelease, bool isMine) + public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false) { // Update Judgement count CurrentJudgements[judgement]++; diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index 8806b772e..a0add18c3 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -300,7 +300,7 @@ private void HandleKeyPressesInFrame() var hitDifference = hitObject.StartTime - Time; // Calculate Score. - var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press, false); + var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press, isMine: false); switch (judgement) { @@ -349,7 +349,7 @@ private void HandleKeyPressesInFrame() var hitDifference = hitObject.EndTime - Time; // Calculate Score - var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release, false); + var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release, isMine: false); // LN was released during a hit window. if (judgement != Judgement.Ghost && judgement != Judgement.Miss) From 35dea885f18cd20fc0e00c5cb53d3c484cb78b2e Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Wed, 17 Dec 2025 22:22:32 +0800 Subject: [PATCH 8/9] Insert CalculateScore from Copilot suggestions. Add few docs to explain the logic within VirtualReplayPlayer.cs --- Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs index da057f52e..bda071c0b 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -227,16 +227,22 @@ private void HandleKeyPressesInFrame() .Concat(previousFramePressed.Except(currentFramePressed)) .ToList(); + // Handle mines that were hit between frames. + // The previous frame's pressed keys are held up until now, so [previousFrameTime..Time) + // is the interval to check for mine hits. foreach (var lane in previousFramePressed) { foreach (var mine in ActiveMines) { + // Check for any mines that were hit between the previous and the current frame var endTime = mine.IsLongNote ? mine.EndTime : mine.StartTime; if (mine.Lane == lane + 1 && endTime + ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime && Time >= mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv]) { // Calculate the hit difference. + // If this lane had been pressed before the mine appeared, give maximum early hit window + // If pressed somewhere within the mine's hit window, accurate hit error can be given var hitDifference = mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime ? (int)ScoreProcessor.JudgementWindow[Judgement.Marv] @@ -246,6 +252,7 @@ private void HandleKeyPressesInFrame() var stat = new HitStat(HitStatType.Miss, KeyPressType.Press, mine, Time, Judgement.Miss, hitDifference, ScoreProcessor.Accuracy, ScoreProcessor.Health); + ScoreProcessor.CalculateScore(stat); ScoreProcessor.Stats.Add(stat); // Object needs to be removed from ActiveObjects. @@ -430,16 +437,17 @@ private void HandleMissedHitObjects() } } // Handle missed mines. + // 'Missed' as in the mines were not triggered, meaning a marvelous judgement should be given. foreach (var hitObject in ActiveMines) { var endTime = hitObject.IsLongNote ? hitObject.EndTime : hitObject.StartTime; if (Time > endTime + ScoreProcessor.JudgementWindow[Judgement.Marv]) { // Create a new HitStat to add to the ScoreProcessor. + // Award a Marvelous for successfully avoiding the mine. var stat = new HitStat(HitStatType.Hit, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Marv, 0, ScoreProcessor.Accuracy, ScoreProcessor.Health); - // Add a miss to the score. ScoreProcessor.CalculateScore(stat); ScoreProcessor.Stats.Add(stat); From 1e98496b71b043172d2b4c9307769ab5fca17081 Mon Sep 17 00:00:00 2001 From: WilliamQiufeng Date: Fri, 19 Dec 2025 22:13:57 +0800 Subject: [PATCH 9/9] diffcalc should ignore mines --- .../Rulesets/Keys/DifficultyProcessorKeys.cs | 6 ++++-- Quaver.API/Maps/Qua.cs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Quaver.API/Maps/Processors/Difficulty/Rulesets/Keys/DifficultyProcessorKeys.cs b/Quaver.API/Maps/Processors/Difficulty/Rulesets/Keys/DifficultyProcessorKeys.cs index 021e4918a..eb6fb4edc 100644 --- a/Quaver.API/Maps/Processors/Difficulty/Rulesets/Keys/DifficultyProcessorKeys.cs +++ b/Quaver.API/Maps/Processors/Difficulty/Rulesets/Keys/DifficultyProcessorKeys.cs @@ -137,7 +137,7 @@ public DifficultyProcessorKeys(Qua map, StrainConstants constants, ModIdentifier StrainConstants = (StrainConstantsKeys)constants; // Don't bother calculating map difficulty if there's less than 2 hit objects - if (map.HitObjects.Count < 2) + if (map.DifficultyContributingHitObjects < 2) return; // Solve for difficulty @@ -203,6 +203,8 @@ private void ComputeBaseStrainStates(float rate, Hand assumeHand) { if (Map.HasScratchKey && Map.HitObjects[i].Lane == Map.GetKeyCount()) continue; + if (Map.HitObjects[i].Type == HitObjectType.Mine) + continue; var curHitOb = new StrainSolverHitObject(Map.HitObjects[i]); var curStrainData = new StrainSolverData(curHitOb, rate); @@ -675,7 +677,7 @@ private float CalculateOverallDifficulty() private void ComputeNoteDensityData(float rate) { //MapLength = Qua.Length; - AverageNoteDensity = SECONDS_TO_MILLISECONDS * Map.HitObjects.Count / (Map.Length * (-.5f * rate + 1.5f)); + AverageNoteDensity = SECONDS_TO_MILLISECONDS * Map.DifficultyContributingHitObjects / (Map.Length * (-.5f * rate + 1.5f)); } /// diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 09925b9fa..67256f48d 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -185,6 +185,19 @@ public List ScrollSpeedFactors /// public List HitObjects { get; private set; } = new List(); + /// + /// Number of mines in the map + /// + [YamlIgnore] + public int MineCount => HitObjects.Count(x => x.Type is HitObjectType.Mine); + + /// + /// Number of notes counted for diffcalc. + /// Currently, it's every note except mines + /// + [YamlIgnore] + public int DifficultyContributingHitObjects => HitObjects.Count - MineCount; + public Dictionary TimingGroups { get; private set; } = new Dictionary();