diff --git a/Quaver.API/Enums/HitObjectType.cs b/Quaver.API/Enums/HitObjectType.cs new file mode 100644 index 000000000..0e9a48027 --- /dev/null +++ b/Quaver.API/Enums/HitObjectType.cs @@ -0,0 +1,11 @@ +namespace Quaver.API.Enums +{ + /// + /// Indicates the type of a hit object + /// + public enum HitObjectType + { + 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 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 f679c4ff9..47fd8ded6 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 { @@ -173,7 +174,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 calculateAllStats = true, + bool isMine = false) { var absoluteDifference = 0; @@ -222,18 +225,25 @@ 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; } + 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]++; @@ -257,7 +267,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) @@ -372,17 +384,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 e51b41e68..09925b9fa 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) => @@ -1101,8 +1102,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 3458e0d39..75d183d17 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 1e07de56a..da057f52e 100644 --- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs +++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs @@ -33,6 +33,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. @@ -96,8 +108,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. InputKeyStore = new List(); @@ -127,10 +153,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.CalculateScore(hitStat); - ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime, - Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); + ScoreProcessor.Stats.Add(hitStat); if (!ScoreProcessor.Failed) continue; @@ -150,6 +178,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) { @@ -186,6 +215,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++) @@ -196,6 +227,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) { @@ -221,7 +279,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, isMine: false); switch (judgement) { @@ -233,7 +291,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)); @@ -270,7 +328,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, isMine: false); // LN was released during a hit window. if (judgement != Judgement.Ghost && judgement != Judgement.Miss) @@ -284,7 +342,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, @@ -302,6 +360,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)); } /// @@ -346,19 +405,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); } @@ -369,10 +429,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]) + { + // 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); + } + 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)); } /// 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);