Skip to content
11 changes: 11 additions & 0 deletions Quaver.API/Enums/HitObjectType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Quaver.API.Enums
{
/// <summary>
/// Indicates the type of a hit object
/// </summary>
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.
}
}
2 changes: 1 addition & 1 deletion Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null)
/// <summary>
/// Adds a judgement to the score and recalculates the score.
/// </summary>
public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false);
public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false);

/// <summary>
/// Calculates the accuracy of the current play session.
Expand Down
32 changes: 17 additions & 15 deletions Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -173,7 +174,9 @@ public ScoreProcessorKeys(Replay replay, JudgementWindows windows = null) : base
/// <param name="hitDifference"></param>
/// <param name="keyPressType"></param>
/// <param name="calculateAllStats"></param>
public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true)
/// <param name="isMine"></param>
public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true,
bool isMine = false)
{
var absoluteDifference = 0;

Expand Down Expand Up @@ -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);
}

/// <inheritdoc />
/// <summary>
/// Calculate Score and Health increase/decrease with a given judgement.
/// </summary>
/// <param name="judgement"></param>
/// <param name="isLongNoteRelease"></param>
public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false)
/// <param name="isMine"></param>
public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false)
{
// Update Judgement count
CurrentJudgements[judgement]++;
Expand All @@ -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)
Expand Down Expand Up @@ -372,17 +384,7 @@ protected override void InitializeHealthWeighting()
/// <returns></returns>
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);
}

/// <summary>
Expand Down
12 changes: 10 additions & 2 deletions Quaver.API/Maps/Qua.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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;
}
Expand Down
12 changes: 12 additions & 0 deletions Quaver.API/Maps/Structures/HitObjectInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public HitSounds HitSound
set;
}

/// <summary>
/// The hit object could be a normal note or a mine
/// </summary>
public HitObjectType Type { get; [MoonSharpVisible(false)] set; }

/// <summary>
/// Key sounds to play when this object is hit.
/// </summary>
Expand Down Expand Up @@ -95,6 +100,11 @@ public string TimingGroup
[YamlIgnore]
public bool IsLongNote => EndTime > 0;

/// <summary>
/// The number of judgements generated by this object
/// </summary>
[YamlIgnore] public int JudgementCount => IsLongNote && Type != HitObjectType.Mine ? 2 : 1;

/// <summary>
/// Returns if the object is allowed to be edited in lua scripts
/// </summary>
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Quaver.API/Replays/Replay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down
106 changes: 94 additions & 12 deletions Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ public class VirtualReplayPlayer
/// The score processor for the virtual replay.
/// </summary>
public ScoreProcessorKeys ScoreProcessor { get; }


/// <summary>
/// All of the mines that are currently active and available.
/// </summary>
public List<HitObjectInfo> ActiveMines { get; }


/// <summary>
/// The list of active mines that are scheduled for removal.
/// </summary>
public List<HitObjectInfo> ActiveMinesToRemove { get; set; }

/// <summary>
/// All of the HitObjects that are currently active and available.
Expand Down Expand Up @@ -96,8 +108,22 @@ public VirtualReplayPlayer(Replay replay, Qua map, JudgementWindows windows = nu

ActiveHitObjects = new List<HitObjectInfo>();
ActiveHeldLongNotes = new List<HitObjectInfo>();
ActiveMines = new List<HitObjectInfo>();

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<VirtualReplayKeyBinding>();
Expand Down Expand Up @@ -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;
Expand All @@ -150,6 +178,7 @@ public void PlayNextFrame()
// Store the objects that need to be removed from the list of active objects.
ActiveHitObjectsToRemove = new List<HitObjectInfo>();
ActiveHeldLongNotesToRemove = new List<HitObjectInfo>();
ActiveMinesToRemove = new List<HitObjectInfo>();

if (CurrentFrame < Replay.Frames.Count)
{
Expand Down Expand Up @@ -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<int>();

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++)
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -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));
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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));
}

/// <summary>
Expand Down Expand Up @@ -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);
}

Expand All @@ -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));
}

/// <summary>
Expand Down
Loading
Loading