From 5454998d154a5ab67464feaf6f4589537b4433cc Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Tue, 1 Oct 2024 15:23:27 +0100 Subject: [PATCH 01/34] Fix KeyReleased not being set to null --- .../ProKeys/Engines/YargProKeysEngine.cs | 53 +++++++++++-------- YARG.Core/Engine/ProKeys/ProKeysEngine.cs | 8 +-- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs index ff3c9849f..67c4dfb73 100644 --- a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using YARG.Core.Chart; using YARG.Core.Input; using YARG.Core.Logging; @@ -14,6 +14,10 @@ public YargProKeysEngine(InstrumentDifficulty chart, SyncTrack sync protected override void MutateStateWithInput(GameInput gameInput) { + // These should always be null before inputs are processed + YargLogger.Assert(KeyHitThisUpdate != null); + YargLogger.Assert(KeyReleasedThisUpdate != null); + var action = gameInput.GetAction(); if (action is ProKeysAction.StarPower) @@ -28,11 +32,11 @@ protected override void MutateStateWithInput(GameInput gameInput) { if (gameInput.Button) { - KeyHit = (int) action; + KeyHitThisUpdate = (int) action; } else { - KeyReleased = (int) action; + KeyReleasedThisUpdate = (int) action; } PreviousKeyMask = KeyMask; @@ -53,14 +57,14 @@ protected override void UpdateHitLogic(double time) if (FatFingerTimer.IsActive) { // Fat Fingered key was released before the timer expired - if (KeyReleased == FatFingerKey && !FatFingerTimer.IsExpired(CurrentTime)) + if (KeyReleasedThisUpdate == FatFingerKey && !FatFingerTimer.IsExpired(CurrentTime)) { YargLogger.LogFormatTrace("Released fat fingered key at {0}. Note was hit: {1}", CurrentTime, FatFingerNote!.WasHit); // The note must be hit to disable the timer if (FatFingerNote!.WasHit) { - YargLogger.LogDebug("Disabling fat finger timer as the note has been hit."); + YargLogger.LogTrace("Disabling fat finger timer as the note has been hit. Fat Finger was Ignored."); FatFingerTimer.Disable(); FatFingerKey = null; FatFingerNote = null; @@ -74,13 +78,18 @@ protected override void UpdateHitLogic(double time) var isHoldingWrongKey = (KeyMask & fatFingerKeyMask) == fatFingerKeyMask; - // Overhit if key is still held OR if key is not held but note was not hit either - if (isHoldingWrongKey || (!isHoldingWrongKey && !FatFingerNote!.WasHit)) + // Overhit if key is still held OR note was not hit + if (isHoldingWrongKey || !FatFingerNote!.WasHit) { YargLogger.LogFormatTrace("Overhit due to fat finger with key {0}. KeyMask: {1}. Holding: {2}. WasHit: {3}", FatFingerKey, KeyMask, isHoldingWrongKey, FatFingerNote!.WasHit); Overhit(FatFingerKey!.Value); } + else + { + YargLogger.LogFormatTrace("Fat finger was ignored. KeyMask: {0}. Holding: {1}. WasHit: {2}", + KeyMask, isHoldingWrongKey, FatFingerNote!.WasHit); + } FatFingerTimer.Disable(); FatFingerKey = null; @@ -88,16 +97,14 @@ protected override void UpdateHitLogic(double time) } } - // Quit early if there are no notes left - if (NoteIndex >= Notes.Count) + // Only check note logic if note index is within bounds + if (NoteIndex < Notes.Count) { - KeyHit = null; - KeyReleased = null; - UpdateSustains(); - return; + CheckForNoteHit(); } - CheckForNoteHit(); + KeyHitThisUpdate = null; + KeyReleasedThisUpdate = null; UpdateSustains(); } @@ -132,7 +139,7 @@ protected override void CheckForNoteHit() HitNote(childNote); } - KeyHit = null; + KeyHitThisUpdate = null; } else { @@ -166,7 +173,7 @@ protected override void CheckForNoteHit() foreach (var note in parentNote.AllNotes) { // Go to next note if the key hit does not match the note's key - if (KeyHit != note.Key) + if (KeyHitThisUpdate != note.Key) { continue; } @@ -193,7 +200,7 @@ protected override void CheckForNoteHit() } } - KeyHit = null; + KeyHitThisUpdate = null; break; } } @@ -202,7 +209,7 @@ protected override void CheckForNoteHit() } // If no note was hit but the user hit a key, then over hit - if (KeyHit != null) + if (KeyHitThisUpdate != null) { static ProKeysNote? CheckForAdjacency(ProKeysNote fullNote, int key) { @@ -228,7 +235,7 @@ protected override void CheckForNoteHit() if (parentNote.PreviousNote is not null && CurrentTime - parentNote.PreviousNote.Time < FatFingerTimer.SpeedAdjustedThreshold) { - adjacentNote = CheckForAdjacency(parentNote.PreviousNote, KeyHit.Value); + adjacentNote = CheckForAdjacency(parentNote.PreviousNote, KeyHitThisUpdate.Value); isAdjacent = adjacentNote != null; inWindow = IsNoteInWindow(parentNote.PreviousNote, out _); @@ -236,7 +243,7 @@ protected override void CheckForNoteHit() // Try to fat finger current note (upcoming note) else { - adjacentNote = CheckForAdjacency(parentNote, KeyHit.Value); + adjacentNote = CheckForAdjacency(parentNote, KeyHitThisUpdate.Value); isAdjacent = adjacentNote != null; inWindow = IsNoteInWindow(parentNote, out _); } @@ -245,7 +252,7 @@ protected override void CheckForNoteHit() if (!inWindow || !isAdjacent || isFatFingerActive) { - Overhit(KeyHit.Value); + Overhit(KeyHitThisUpdate.Value); // TODO Maybe don't disable the timer/use a flag saying no more fat fingers allowed for the current note. @@ -256,7 +263,7 @@ protected override void CheckForNoteHit() else { StartTimer(ref FatFingerTimer, CurrentTime); - FatFingerKey = KeyHit.Value; + FatFingerKey = KeyHitThisUpdate.Value; FatFingerNote = adjacentNote; @@ -264,7 +271,7 @@ protected override void CheckForNoteHit() FatFingerTimer.EndTime, FatFingerKey); } - KeyHit = null; + KeyHitThisUpdate = null; } } diff --git a/YARG.Core/Engine/ProKeys/ProKeysEngine.cs b/YARG.Core/Engine/ProKeys/ProKeysEngine.cs index 63386ee30..ff7b4f5a5 100644 --- a/YARG.Core/Engine/ProKeys/ProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/ProKeysEngine.cs @@ -27,12 +27,12 @@ public abstract class ProKeysEngine : BaseEngine /// The integer value for the key that was hit this update. null is none. /// - protected int? KeyHit; + protected int? KeyHitThisUpdate; /// /// The integer value for the key that was released this update. null is none. /// - protected int? KeyReleased; + protected int? KeyReleasedThisUpdate; protected int? FatFingerKey; @@ -95,8 +95,8 @@ public override void Reset(bool keepCurrentButtons = false) KeyPressTimes[i] = -9999; } - KeyHit = null; - KeyReleased = null; + KeyHitThisUpdate = null; + KeyReleasedThisUpdate = null; FatFingerKey = null; From 1cf1d2e9e08eb8977467b16da2287f41d0c1b691 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Tue, 1 Oct 2024 15:26:34 +0100 Subject: [PATCH 02/34] Remove source of inconsistent sustain rebase --- YARG.Core/Engine/BaseEngine.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index ce3c2860b..3db0686af 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using YARG.Core.Chart; @@ -531,8 +531,6 @@ protected void GainStarPower(uint ticks) StarPowerEndTime = GetStarPowerDrainTickToTime(StarPowerTickEndPosition, CurrentSyncTrackState); YargLogger.LogFormatTrace("New end tick and time: {0}, {1}", StarPowerTickEndPosition, StarPowerEndTime); } - - RebaseProgressValues(CurrentTick); } protected void DrainStarPower(uint starPowerTicks) From cb28b471692110ce7bf4d794bd8e6fd2b3d610bb Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Tue, 1 Oct 2024 15:28:01 +0100 Subject: [PATCH 03/34] Fix jumps in star power amount when whammying --- YARG.Core/Engine/BaseEngine.Generic.cs | 22 ++++++++++++------- YARG.Core/Engine/BaseEngine.cs | 8 +++---- .../Guitar/Engines/YargFiveFretEngine.cs | 1 + .../ProKeys/Engines/YargProKeysEngine.cs | 1 + 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index 5df4602f7..d2759d86d 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using YARG.Core.Chart; +using YARG.Core.Extensions; using YARG.Core.Logging; using YARG.Core.Utility; @@ -508,6 +510,9 @@ protected virtual void UpdateSustains() var points = (int) Math.Ceiling(finalScore); AddScore(points); + ulong timeAsUlong = UnsafeExtensions.DoubleToUInt64Bits(CurrentTime); + ulong baseScoreAsUlong = UnsafeExtensions.DoubleToUInt64Bits(sustain.BaseScore); + YargLogger.LogFormatTrace("Added {0} points for end of sustain at {1} (0x{2}). Base Score/Tick: {3} (0x{4}), {5}", points, CurrentTime, timeAsUlong.ToString("X"), sustain.BaseScore, baseScoreAsUlong.ToString("X"), sustain.BaseTick); // SustainPoints must include the multiplier, but NOT the star power multiplier int sustainPoints = points * EngineStats.ScoreMultiplier; @@ -542,14 +547,20 @@ protected virtual void UpdateSustains() if (isStarPowerSustainActive && StarPowerWhammyTimer.IsActive) { - var whammyTicks = CurrentTick - LastStarPowerWhammyTick; + var whammyTicks = CurrentTick - LastTick; + + // Just started whammying, award 1 tick + if (!LastWhammyTimerState) + { + whammyTicks = 1; + } GainStarPower(whammyTicks); EngineStats.StarPowerWhammyTicks += whammyTicks; - - LastStarPowerWhammyTick = CurrentTick; } + LastWhammyTimerState = StarPowerWhammyTimer.IsActive; + // Whammy is disabled after sustains are updated. // This is because all the ticks that have accumulated will have been accounted for when it is disabled. // Whereas disabling it before could mean there are some ticks which should have been whammied but weren't. @@ -561,11 +572,6 @@ protected virtual void UpdateSustains() protected virtual void StartSustain(TNoteType note) { - if (ActiveSustains.Count == 0) - { - LastStarPowerWhammyTick = CurrentTick; - } - var sustain = new ActiveSustain(note); ActiveSustains.Add(sustain); diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index 3db0686af..94a64a6c5 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using YARG.Core.Chart; @@ -35,8 +35,6 @@ public abstract class BaseEngine public SoloStartEvent? OnSoloStart; public SoloEndEvent? OnSoloEnd; - public bool IsInputQueued => InputQueue.Count > 0; - public bool CanStarPowerActivate => BaseStats.StarPowerTickAmount >= TicksPerHalfSpBar; public int BaseScore { get; protected set; } @@ -87,7 +85,7 @@ public abstract class BaseEngine protected EngineTimer StarPowerWhammyTimer; - public uint LastStarPowerWhammyTick { get; protected set; } + protected bool LastWhammyTimerState; public uint StarPowerTickPosition { get; protected set; } public uint PreviousStarPowerTickPosition { get; protected set; } @@ -344,7 +342,7 @@ protected virtual void GenerateQueuedUpdates(double nextTime) { if (IsTimeBetween(StarPowerEndTime, previousTime, nextTime)) { - YargLogger.LogFormatDebug("Queuing Star Power End Time at {0}", StarPowerEndTime); + YargLogger.LogFormatTrace("Queuing Star Power End Time at {0}", StarPowerEndTime); QueueUpdateTime(StarPowerEndTime, "SP End Time"); } } diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index 195a892bc..4050fc870 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -72,6 +72,7 @@ protected override void MutateStateWithInput(GameInput gameInput) } else if (action is GuitarAction.Whammy) { + LastWhammyTimerState = StarPowerWhammyTimer.IsActive; StarPowerWhammyTimer.Start(gameInput.Time); } else if (action is GuitarAction.StrumDown or GuitarAction.StrumUp && gameInput.Button) diff --git a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs index 67c4dfb73..4c82a31e7 100644 --- a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs @@ -26,6 +26,7 @@ protected override void MutateStateWithInput(GameInput gameInput) } else if (action is ProKeysAction.TouchEffects) { + LastWhammyTimerState = StarPowerWhammyTimer.IsActive; StarPowerWhammyTimer.Start(gameInput.Time); } else From 0fd2fc14f390d511c39cc6d6709665ab23ff4b09 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:10:36 +0100 Subject: [PATCH 04/34] Add documentation for SP stats --- YARG.Core/Engine/BaseStats.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/YARG.Core/Engine/BaseStats.cs b/YARG.Core/Engine/BaseStats.cs index fb093f694..3bbecf700 100644 --- a/YARG.Core/Engine/BaseStats.cs +++ b/YARG.Core/Engine/BaseStats.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using YARG.Core.Extensions; using YARG.Core.Replays; @@ -90,23 +90,38 @@ public abstract class BaseStats /// public virtual float Percent => TotalNotes == 0 ? 1f : (float) NotesHit / TotalNotes; + /// + /// Current amount of Star Power ticks the player has. + /// public uint StarPowerTickAmount; + /// + /// Total ticks of Star Power earned. + /// public uint TotalStarPowerTicks; + /// + /// Total bars of Star Power filled. + /// public double TotalStarPowerBarsFilled; + /// + /// Number of times the player has activated Star Power. + /// public int StarPowerActivationCount; + /// + /// Total amount of time the player has been in Star Power. + /// public double TimeInStarPower; /// - /// Amount of Star Power/Overdrive gained from whammy during the current whammy period. + /// Amount of Star Power/Overdrive gained from whammy. /// public uint StarPowerWhammyTicks; /// - /// True if the player currently has Star Power/Overdrive active. + /// True if the player currently has Star Power active. /// public bool IsStarPowerActive; @@ -131,6 +146,9 @@ public abstract class BaseStats /// public int SoloBonuses; + /// + /// Amount of points earned from Star Power. + /// public int StarPowerScore; /// From d433709ba675b73d22dfa394046813e765ec0070 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:12:01 +0100 Subject: [PATCH 05/34] Write missing stats and move SustainScore to base --- ReplayCli/Cli.cs | 6 ++++-- YARG.Core/Engine/BaseStats.cs | 14 +++++++++++++- YARG.Core/Engine/Guitar/GuitarStats.cs | 4 ---- YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs | 13 ++++++------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ReplayCli/Cli.cs b/ReplayCli/Cli.cs index 0c70263b5..74a09b0c1 100644 --- a/ReplayCli/Cli.cs +++ b/ReplayCli/Cli.cs @@ -200,6 +200,9 @@ static void PrintStatDifference(string name, T frameStat, T resultStat) Console.WriteLine($"Base stats:"); PrintStatDifference("CommittedScore", originalStats.CommittedScore, resultStats.CommittedScore); PrintStatDifference("PendingScore", originalStats.PendingScore, resultStats.PendingScore); + PrintStatDifference("NoteScore", originalStats.NoteScore, resultStats.NoteScore); + PrintStatDifference("SustainScore", originalStats.SustainScore, resultStats.SustainScore); + PrintStatDifference("MultiplierScore", originalStats.MultiplierScore, resultStats.MultiplierScore); PrintStatDifference("TotalScore", originalStats.TotalScore, resultStats.TotalScore); PrintStatDifference("StarScore", originalStats.StarScore, resultStats.StarScore); PrintStatDifference("Combo", originalStats.Combo, resultStats.Combo); @@ -210,6 +213,7 @@ static void PrintStatDifference(string name, T frameStat, T resultStat) PrintStatDifference("NotesMissed", originalStats.NotesMissed, resultStats.NotesMissed); PrintStatDifference("Percent", originalStats.Percent, resultStats.Percent); PrintStatDifference("StarPowerTickAmount", originalStats.StarPowerTickAmount, resultStats.StarPowerTickAmount); + PrintStatDifference("StarPowerWhammyTicks", originalStats.StarPowerWhammyTicks, resultStats.StarPowerWhammyTicks); PrintStatDifference("TotalStarPowerTicks", originalStats.TotalStarPowerTicks, resultStats.TotalStarPowerTicks); PrintStatDifference("TimeInStarPower", originalStats.TimeInStarPower, resultStats.TimeInStarPower); PrintStatDifference("IsStarPowerActive", originalStats.IsStarPowerActive, resultStats.IsStarPowerActive); @@ -229,8 +233,6 @@ static void PrintStatDifference(string name, T frameStat, T resultStat) PrintStatDifference("Overstrums", originalGuitar.Overstrums, resultGuitar.Overstrums); PrintStatDifference("HoposStrummed", originalGuitar.HoposStrummed, resultGuitar.HoposStrummed); PrintStatDifference("GhostInputs", originalGuitar.GhostInputs, resultGuitar.GhostInputs); - PrintStatDifference("StarPowerWhammyTicks", originalGuitar.StarPowerWhammyTicks, resultGuitar.StarPowerWhammyTicks); - PrintStatDifference("SustainScore", originalGuitar.SustainScore, resultGuitar.SustainScore); break; } case (DrumsStats originalDrums, DrumsStats resultDrums): diff --git a/YARG.Core/Engine/BaseStats.cs b/YARG.Core/Engine/BaseStats.cs index 3bbecf700..05b308ff2 100644 --- a/YARG.Core/Engine/BaseStats.cs +++ b/YARG.Core/Engine/BaseStats.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using YARG.Core.Extensions; using YARG.Core.Replays; @@ -164,6 +164,9 @@ protected BaseStats(BaseStats stats) { CommittedScore = stats.CommittedScore; PendingScore = stats.PendingScore; + NoteScore = stats.NoteScore; + SustainScore = stats.SustainScore; + MultiplierScore = stats.MultiplierScore; Combo = stats.Combo; MaxCombo = stats.MaxCombo; ScoreMultiplier = stats.ScoreMultiplier; @@ -191,6 +194,9 @@ protected BaseStats(UnmanagedMemoryStream stream, int version) { CommittedScore = stream.Read(Endianness.Little); PendingScore = stream.Read(Endianness.Little); + NoteScore = stream.Read(Endianness.Little); + SustainScore = stream.Read(Endianness.Little); + MultiplierScore = stream.Read(Endianness.Little); Combo = stream.Read(Endianness.Little); MaxCombo = stream.Read(Endianness.Little); @@ -219,6 +225,9 @@ public virtual void Reset() { CommittedScore = 0; PendingScore = 0; + NoteScore = 0; + SustainScore = 0; + MultiplierScore = 0; Combo = 0; MaxCombo = 0; ScoreMultiplier = 1; @@ -247,6 +256,9 @@ public virtual void Serialize(BinaryWriter writer) { writer.Write(CommittedScore); writer.Write(PendingScore); + writer.Write(NoteScore); + writer.Write(SustainScore); + writer.Write(MultiplierScore); writer.Write(Combo); writer.Write(MaxCombo); diff --git a/YARG.Core/Engine/Guitar/GuitarStats.cs b/YARG.Core/Engine/Guitar/GuitarStats.cs index ce257cfaf..5247d4292 100644 --- a/YARG.Core/Engine/Guitar/GuitarStats.cs +++ b/YARG.Core/Engine/Guitar/GuitarStats.cs @@ -30,7 +30,6 @@ public GuitarStats(GuitarStats stats) : base(stats) Overstrums = stats.Overstrums; HoposStrummed = stats.HoposStrummed; GhostInputs = stats.GhostInputs; - SustainScore = stats.SustainScore; } public GuitarStats(UnmanagedMemoryStream stream, int version) @@ -39,7 +38,6 @@ public GuitarStats(UnmanagedMemoryStream stream, int version) Overstrums = stream.Read(Endianness.Little); HoposStrummed = stream.Read(Endianness.Little); GhostInputs = stream.Read(Endianness.Little); - SustainScore = stream.Read(Endianness.Little); } public override void Reset() @@ -48,7 +46,6 @@ public override void Reset() Overstrums = 0; HoposStrummed = 0; GhostInputs = 0; - SustainScore = 0; } public override void Serialize(BinaryWriter writer) @@ -58,7 +55,6 @@ public override void Serialize(BinaryWriter writer) writer.Write(Overstrums); writer.Write(HoposStrummed); writer.Write(GhostInputs); - writer.Write(SustainScore); } public override ReplayStats ConstructReplayStats(string name) diff --git a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs index 320f3f8c5..930a6f0d2 100644 --- a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs +++ b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using YARG.Core.Chart; @@ -352,20 +352,19 @@ private static bool IsPassResult(BaseStats original, BaseStats result) instrumentPass = originalGuitar.Overstrums == resultGuitar.Overstrums && originalGuitar.GhostInputs == resultGuitar.GhostInputs && originalGuitar.HoposStrummed == resultGuitar.HoposStrummed && - originalGuitar.StarPowerWhammyTicks == resultGuitar.StarPowerWhammyTicks && - originalGuitar.SustainScore == resultGuitar.SustainScore; + originalGuitar.StarPowerWhammyTicks == resultGuitar.StarPowerWhammyTicks; - YargLogger.LogFormatDebug("Guitar:\nOverstrums: {0} == {1}\nGhost Inputs: {2} == {3}\nHOPOs Strummed: {4} == {5}\n" + + YargLogger.LogFormatDebug( + "Guitar:\nOverstrums: {0} == {1}\nGhost Inputs: {2} == {3}\nHOPOs Strummed: {4} == {5}\n" + "Whammy Ticks: {6} == {7}\nSustain Points: {8} == {9}", originalGuitar.Overstrums, resultGuitar.Overstrums, originalGuitar.GhostInputs, resultGuitar.GhostInputs, - originalGuitar.HoposStrummed, resultGuitar.HoposStrummed, - originalGuitar.StarPowerWhammyTicks, resultGuitar.StarPowerWhammyTicks, - originalGuitar.SustainScore, resultGuitar.SustainScore); + originalGuitar.HoposStrummed, resultGuitar.HoposStrummed); } bool generalPass = original.CommittedScore == result.CommittedScore && original.NotesHit == result.NotesHit && + original.SustainScore == result.SustainScore && original.NotesMissed == result.NotesMissed && original.Combo == result.Combo && original.MaxCombo == result.MaxCombo && From 665416353cd49306cd0c81a3731164cefaaed276 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:13:03 +0100 Subject: [PATCH 06/34] Track ignored fat fingers stat --- YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs | 5 ++++- YARG.Core/Engine/ProKeys/ProKeysStats.cs | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs index 4c82a31e7..e73b2a1fd 100644 --- a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using YARG.Core.Chart; using YARG.Core.Input; using YARG.Core.Logging; @@ -69,6 +69,8 @@ protected override void UpdateHitLogic(double time) FatFingerTimer.Disable(); FatFingerKey = null; FatFingerNote = null; + + EngineStats.FatFingersIgnored++; } } else if(FatFingerTimer.IsExpired(CurrentTime)) @@ -88,6 +90,7 @@ protected override void UpdateHitLogic(double time) } else { + EngineStats.FatFingersIgnored++; YargLogger.LogFormatTrace("Fat finger was ignored. KeyMask: {0}. Holding: {1}. WasHit: {2}", KeyMask, isHoldingWrongKey, FatFingerNote!.WasHit); } diff --git a/YARG.Core/Engine/ProKeys/ProKeysStats.cs b/YARG.Core/Engine/ProKeys/ProKeysStats.cs index 37be5ed37..f1d0ed9a5 100644 --- a/YARG.Core/Engine/ProKeys/ProKeysStats.cs +++ b/YARG.Core/Engine/ProKeys/ProKeysStats.cs @@ -11,6 +11,11 @@ public class ProKeysStats : BaseStats /// public int Overhits; + /// + /// Amount of overhits which were ignored due to fat-fingering. + /// + public int FatFingersIgnored; + public ProKeysStats() { } @@ -18,18 +23,21 @@ public ProKeysStats() public ProKeysStats(ProKeysStats stats) : base(stats) { Overhits = stats.Overhits; + FatFingersIgnored = stats.FatFingersIgnored; } public ProKeysStats(UnmanagedMemoryStream stream, int version) : base(stream, version) { Overhits = stream.Read(Endianness.Little); + FatFingersIgnored = stream.Read(Endianness.Little); } public override void Reset() { base.Reset(); Overhits = 0; + FatFingersIgnored = 0; } public override void Serialize(BinaryWriter writer) @@ -37,6 +45,7 @@ public override void Serialize(BinaryWriter writer) base.Serialize(writer); writer.Write(Overhits); + writer.Write(FatFingersIgnored); } public override ReplayStats ConstructReplayStats(string name) From 4ad5412e5f666ff127be946210dcf8a721fc2065 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:13:36 +0100 Subject: [PATCH 07/34] Add Drums dynamics stat tracking --- YARG.Core/Engine/Drums/DrumsEngine.cs | 15 ++++++ YARG.Core/Engine/Drums/DrumsStats.cs | 49 +++++++++++++++++++ .../Engine/Drums/Engines/YargDrumsEngine.cs | 10 ++++ 3 files changed, 74 insertions(+) diff --git a/YARG.Core/Engine/Drums/DrumsEngine.cs b/YARG.Core/Engine/Drums/DrumsEngine.cs index 3c8d3155e..fb7534fb3 100644 --- a/YARG.Core/Engine/Drums/DrumsEngine.cs +++ b/YARG.Core/Engine/Drums/DrumsEngine.cs @@ -29,6 +29,21 @@ protected DrumsEngine(InstrumentDifficulty chart, SyncTrack syncTrack, DrumsEngineParameters engineParameters, bool isBot) : base(chart, syncTrack, engineParameters, true, isBot) { + foreach(var note in Notes) + { + foreach(var all in note.AllNotes) + { + if(all.IsAccent) + { + EngineStats.TotalAccents++; + } + else if(all.IsGhost) + { + EngineStats.TotalGhosts++; + } + } + } + GetWaitCountdowns(Notes); } diff --git a/YARG.Core/Engine/Drums/DrumsStats.cs b/YARG.Core/Engine/Drums/DrumsStats.cs index d4ade553d..172514216 100644 --- a/YARG.Core/Engine/Drums/DrumsStats.cs +++ b/YARG.Core/Engine/Drums/DrumsStats.cs @@ -11,6 +11,31 @@ public class DrumsStats : BaseStats /// public int Overhits; + /// + /// Number of ghosts the player hit with correct dynamics. + /// + public int GhostsHit; + + /// + /// Total number of ghost notes in the chart. + /// + public int TotalGhosts; + + /// + /// Number of accents the player hit with correct dynamics. + /// + public int AccentsHit; + + /// + /// Total number of accent notes in the chart. + /// + public int TotalAccents; + + /// + /// Amount of points earned from hitting notes with correct dynamics. + /// + public int DynamicsBonus; + public DrumsStats() { } @@ -18,18 +43,37 @@ public DrumsStats() public DrumsStats(DrumsStats stats) : base(stats) { Overhits = stats.Overhits; + GhostsHit = stats.GhostsHit; + TotalGhosts = stats.TotalGhosts; + AccentsHit = stats.AccentsHit; + TotalAccents = stats.TotalAccents; + DynamicsBonus = stats.DynamicsBonus; } public DrumsStats(UnmanagedMemoryStream stream, int version) : base(stream, version) { Overhits = stream.Read(Endianness.Little); + GhostsHit = stream.Read(Endianness.Little); + TotalGhosts = stream.Read(Endianness.Little); + AccentsHit = stream.Read(Endianness.Little); + TotalAccents = stream.Read(Endianness.Little); + DynamicsBonus = stream.Read(Endianness.Little); } public override void Reset() { base.Reset(); Overhits = 0; + GhostsHit = 0; + // Don't reset TotalGhosts + // TotalGhosts = 0; + + AccentsHit = 0; + // Don't reset TotalAccents + // TotalAccents = 0; + + DynamicsBonus = 0; } public override void Serialize(BinaryWriter writer) @@ -37,6 +81,11 @@ public override void Serialize(BinaryWriter writer) base.Serialize(writer); writer.Write(Overhits); + writer.Write(GhostsHit); + writer.Write(TotalGhosts); + writer.Write(AccentsHit); + writer.Write(TotalAccents); + writer.Write(DynamicsBonus); } public override ReplayStats ConstructReplayStats(string name) diff --git a/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs b/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs index 2710ec63c..dd2820c8a 100644 --- a/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs +++ b/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs @@ -95,7 +95,17 @@ protected override void CheckForNoteHit() { const int velocityBonus = POINTS_PER_NOTE / 2; AddScore(velocityBonus); + EngineStats.DynamicsBonus += velocityBonus; YargLogger.LogFormatTrace("Velocity bonus of {0} points was awarded to a note at tick {1}.", velocityBonus, note.Tick); + + if(note.IsAccent) + { + EngineStats.AccentsHit++; + } + else if(note.IsGhost) + { + EngineStats.GhostsHit++; + } } ResetPadState(); From af9046a6d7e21e3a6cbcec2f0be57e5b2ab393ca Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:14:17 +0100 Subject: [PATCH 08/34] Print stats in ReplayCli despite passing --- ReplayCli/Cli.Verify.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ReplayCli/Cli.Verify.cs b/ReplayCli/Cli.Verify.cs index 0b67ea222..a525b53a9 100644 --- a/ReplayCli/Cli.Verify.cs +++ b/ReplayCli/Cli.Verify.cs @@ -25,7 +25,7 @@ private bool RunVerify() // Print result data var bandScore = results.Sum(x => x.ResultStats.TotalScore); - if (bandScore != _replayInfo.BandScore) + if (results.Any(x => !x.Passed)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("VERIFICATION FAILED!"); @@ -62,6 +62,18 @@ private bool RunVerify() Console.WriteLine($"Metadata score : {_replayInfo.BandScore}"); Console.WriteLine($"Real score : {bandScore}"); Console.WriteLine($"Difference : {Math.Abs(bandScore - _replayInfo.BandScore)}\n"); + + for (int frameIndex = 0; frameIndex < _replayData.Frames.Length; frameIndex++) + { + var frame = _replayData.Frames[frameIndex]; + var result = results[frameIndex]; + + Console.WriteLine($"-------------"); + Console.WriteLine($"Frame {frameIndex + 1}"); + Console.WriteLine($"-------------"); + PrintStatDifferences(frame.Stats, result.ResultStats); + Console.WriteLine(); + } return true; } } From 1439c1d691dedb2dc3c57b01ec99e66e30dc5140 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 25 Oct 2024 15:14:36 +0100 Subject: [PATCH 09/34] Add new stats to output for ReplayCli --- ReplayCli/Cli.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ReplayCli/Cli.cs b/ReplayCli/Cli.cs index 74a09b0c1..f260f3297 100644 --- a/ReplayCli/Cli.cs +++ b/ReplayCli/Cli.cs @@ -239,6 +239,11 @@ static void PrintStatDifference(string name, T frameStat, T resultStat) { Console.WriteLine("Drums stats:"); PrintStatDifference("Overhits", originalDrums.Overhits, resultDrums.Overhits); + PrintStatDifference("GhostsHit", originalDrums.GhostsHit, resultDrums.GhostsHit); + PrintStatDifference("TotalGhosts", originalDrums.TotalGhosts, resultDrums.TotalGhosts); + PrintStatDifference("AccentsHit", originalDrums.AccentsHit, resultDrums.AccentsHit); + PrintStatDifference("TotalAccents", originalDrums.TotalAccents, resultDrums.TotalAccents); + PrintStatDifference("DynamicsBonus", originalDrums.DynamicsBonus, resultDrums.DynamicsBonus); break; } case (VocalsStats originalVocals, VocalsStats resultVocals): @@ -253,6 +258,7 @@ static void PrintStatDifference(string name, T frameStat, T resultStat) { Console.WriteLine("Pro Keys stats:"); PrintStatDifference("Overhits", originalKeys.Overhits, resultKeys.Overhits); + PrintStatDifference("FatFingersIgnored", originalKeys.FatFingersIgnored, resultKeys.FatFingersIgnored); break; } default: From 88ea636233483b3eb77050bbaa460621a799b246 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Oct 2024 17:49:36 -0600 Subject: [PATCH 10/34] Refactor replay analyzer stat logging; implement proper validation for each instrument type --- YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs | 287 ++++++++++++++++--- 1 file changed, 253 insertions(+), 34 deletions(-) diff --git a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs index 930a6f0d2..4bd15ca62 100644 --- a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs +++ b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs @@ -13,6 +13,7 @@ using YARG.Core.Engine.Vocals.Engines; using YARG.Core.Game; using YARG.Core.Logging; +using Cysharp.Text; namespace YARG.Core.Replays.Analyzer { @@ -328,54 +329,272 @@ private List GenerateFrameTimes(double from, double to) return times; } + // ReSharper disable CompareOfFloatsByEqualityOperator + private static bool IsPassResult(BaseStats original, BaseStats result) { - YargLogger.LogFormatDebug("Score: {0} == {1}\nHit: {2} == {3}\nMissed: {4} == {5}\nCombo: {6} == {7}\nMaxCombo: {8} == {9}\n", - original.CommittedScore, result.CommittedScore, - original.NotesHit, result.NotesHit, - original.NotesMissed, result.NotesMissed, - original.Combo, result.Combo, - original.MaxCombo, result.MaxCombo); - - YargLogger.LogFormatDebug("Solo: {0} == {1}\nSP Bonus: {2} == {3}\nSP Phrases: {4} == {5}\n" + - "Time In SP: {6} == {7}\nSP Ticks: {8} == {9}", - original.SoloBonuses, result.SoloBonuses, - original.StarPowerScore, result.StarPowerScore, - original.StarPowerPhrasesHit, result.StarPowerPhrasesHit, - original.TimeInStarPower, result.TimeInStarPower, - original.TotalStarPowerTicks, result.TotalStarPowerTicks); - - bool instrumentPass = true; - - if(original is GuitarStats originalGuitar && result is GuitarStats resultGuitar) + // For easier maintenance/reading, manually check log level and + // use a string builder instead of LogFormat methods + if (YargLogger.MinimumLogLevel <= LogLevel.Debug) { - instrumentPass = originalGuitar.Overstrums == resultGuitar.Overstrums && - originalGuitar.GhostInputs == resultGuitar.GhostInputs && - originalGuitar.HoposStrummed == resultGuitar.HoposStrummed && - originalGuitar.StarPowerWhammyTicks == resultGuitar.StarPowerWhammyTicks; - - YargLogger.LogFormatDebug( - "Guitar:\nOverstrums: {0} == {1}\nGhost Inputs: {2} == {3}\nHOPOs Strummed: {4} == {5}\n" + - "Whammy Ticks: {6} == {7}\nSustain Points: {8} == {9}", - originalGuitar.Overstrums, resultGuitar.Overstrums, - originalGuitar.GhostInputs, resultGuitar.GhostInputs, - originalGuitar.HoposStrummed, resultGuitar.HoposStrummed); + using var builder = new Utf16ValueStringBuilder(true); + + // This helper is copied to the instrument-specific methods + // because passing a `using` variable by reference is disallowed, + // and working around that is annoying + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); + } + + // Commented stats aren't serialized + + builder.AppendLine("Scoring:"); + FormatStat("Committed score", original.CommittedScore, result.CommittedScore); + FormatStat("Pending score", original.PendingScore, result.PendingScore); + FormatStat("Score from notes", original.NoteScore, result.NoteScore); + FormatStat("Score from sustains", original.SustainScore, result.SustainScore); + FormatStat("Score from multipliers", original.MultiplierScore, result.MultiplierScore); + FormatStat("Score from solos", original.SoloBonuses, result.SoloBonuses); + FormatStat("Score from SP", original.StarPowerScore, result.StarPowerScore); + // FormatStat("Stars", original.Stars, result.Stars); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + + builder.AppendLine("Performance:"); + FormatStat("Notes hit", original.NotesHit, result.NotesHit); + FormatStat("Notes missed", original.NotesMissed, result.NotesMissed); + FormatStat("Combo", original.Combo, result.Combo); + FormatStat("Max combo", original.MaxCombo, result.MaxCombo); + FormatStat("Multiplier", original.ScoreMultiplier, result.ScoreMultiplier); + FormatStat("Hit percent", original.Percent, result.Percent); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + + builder.AppendLine("Star Power:"); + FormatStat("Phrases hit", original.StarPowerPhrasesHit, result.StarPowerPhrasesHit); + FormatStat("Phrases missed", original.StarPowerPhrasesMissed, result.StarPowerPhrasesMissed); + FormatStat("Total ticks earned", original.TotalStarPowerTicks, result.TotalStarPowerTicks); + FormatStat("Remaining ticks", original.StarPowerTickAmount, result.StarPowerTickAmount); + FormatStat("Ticks from whammy", original.StarPowerWhammyTicks, result.StarPowerWhammyTicks); + FormatStat("Time in SP", original.TimeInStarPower, result.TimeInStarPower); + // FormatStat("Activation count", original.StarPowerActivationCount, result.StarPowerActivationCount); + // FormatStat("Total bars filled", original.TotalStarPowerBarsFilled, result.TotalStarPowerBarsFilled); + FormatStat("Ended with SP active", original.IsStarPowerActive, result.IsStarPowerActive); + YargLogger.LogDebug(builder.ToString()); } - bool generalPass = original.CommittedScore == result.CommittedScore && + bool scoringPass = + original.CommittedScore == result.CommittedScore && + original.PendingScore == result.PendingScore && + original.NoteScore == result.NoteScore && + original.SustainScore == result.SustainScore && + original.MultiplierScore == result.MultiplierScore && + original.SoloBonuses == result.SoloBonuses && + original.StarPowerScore == result.StarPowerScore; // && + // original.Stars == result.Stars; + + bool performancePass = original.NotesHit == result.NotesHit && original.SustainScore == result.SustainScore && original.NotesMissed == result.NotesMissed && original.Combo == result.Combo && original.MaxCombo == result.MaxCombo && - original.SoloBonuses == result.SoloBonuses && - original.StarPowerScore == result.StarPowerScore && + original.ScoreMultiplier == result.ScoreMultiplier && + original.Percent == result.Percent; + + bool spPass = original.StarPowerPhrasesHit == result.StarPowerPhrasesHit && - // ReSharper disable once CompareOfFloatsByEqualityOperator + original.StarPowerPhrasesMissed == result.StarPowerPhrasesMissed && + original.IsStarPowerActive == result.IsStarPowerActive && + original.StarPowerTickAmount == result.StarPowerTickAmount && + original.StarPowerWhammyTicks == result.StarPowerWhammyTicks && original.TimeInStarPower == result.TimeInStarPower && - original.TotalStarPowerTicks == result.TotalStarPowerTicks; + // original.StarPowerActivationCount == result.StarPowerActivationCount && + // original.TotalStarPowerBarsFilled == result.TotalStarPowerBarsFilled && + original.IsStarPowerActive == result.IsStarPowerActive; + + bool generalPass = scoringPass && performancePass && spPass; + + bool instrumentPass; + switch (original, result) + { + case (GuitarStats guitar1, GuitarStats guitar2): + instrumentPass = IsInstrumentPassResult(guitar1, guitar2); + break; + + case (DrumsStats drums1, DrumsStats drums2): + instrumentPass = IsInstrumentPassResult(drums1, drums2); + break; + + case (VocalsStats vox1, VocalsStats vox2): + instrumentPass = IsInstrumentPassResult(vox1, vox2); + break; + + // case (ProGuitarStats pg1, ProGuitarStats pg2): + // instrumentPass = IsInstrumentPassResult(pg1, pg2); + // break; + + case (ProKeysStats pk1, ProKeysStats pk2): + instrumentPass = IsInstrumentPassResult(pk1, pk2); + break; + + default: + YargLogger.Assert(original.GetType() == result.GetType()); + YargLogger.LogFormatDebug("Instrument-specific validation not yet implemented for {0}", original.GetType()); + instrumentPass = true; + break; + } return generalPass && instrumentPass; } + + private static bool IsInstrumentPassResult(GuitarStats original, GuitarStats result) + { + if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + { + using var builder = new Utf16ValueStringBuilder(true); + + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); + } + + builder.AppendLine("Guitar:"); + FormatStat("Overstrums", original.Overstrums, result.Overstrums); + FormatStat("Ghost inputs", original.GhostInputs, result.GhostInputs); + FormatStat("HOPOs strummed", original.HoposStrummed, result.HoposStrummed); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + } + + return original.Overstrums == result.Overstrums && + original.GhostInputs == result.GhostInputs && + original.HoposStrummed == result.HoposStrummed; + } + + private static bool IsInstrumentPassResult(DrumsStats original, DrumsStats result) + { + if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + { + using var builder = new Utf16ValueStringBuilder(true); + + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); + } + + builder.AppendLine("Drums:"); + FormatStat("Overhits", original.Overhits, result.Overhits); + FormatStat("Ghosts hit correctly", original.GhostsHit, result.GhostsHit); + FormatStat("Ghosts hit incorrectly", + original.TotalGhosts - original.GhostsHit, + result.TotalGhosts - result.GhostsHit); + FormatStat("Accents hit correctly", original.AccentsHit, result.AccentsHit); + FormatStat("Accents hit incorrectly", + original.TotalAccents - original.AccentsHit, + result.TotalAccents - result.AccentsHit); + FormatStat("Score from dynamics", original.DynamicsBonus, result.DynamicsBonus); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + } + + return original.Overhits == result.Overhits && + original.GhostsHit == result.GhostsHit && + original.TotalGhosts == result.TotalGhosts && + original.AccentsHit == result.AccentsHit && + original.TotalAccents == result.TotalAccents && + original.DynamicsBonus == result.DynamicsBonus; + } + + private static bool IsInstrumentPassResult(VocalsStats original, VocalsStats result) + { + if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + { + using var builder = new Utf16ValueStringBuilder(true); + + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); + } + + builder.AppendLine("Vocals:"); + FormatStat("Note ticks hit", original.TicksHit, result.TicksHit); + FormatStat("Note ticks missed", original.TicksMissed, result.TicksMissed); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + } + + return original.TicksHit == result.TicksHit && + original.TicksMissed == result.TicksMissed; + } + + // private static bool IsInstrumentPassResult(ProGuitarStats original, ProGuitarStats result) + // { + // if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + // { + // using var builder = new Utf16ValueStringBuilder(true); + + // void FormatStat(string stat, T originalValue, T resultValue) + // where T : IEquatable + // { + // string format = originalValue.Equals(resultValue) + // ? "- {0}: {1} == {2}\n" + // : "- {0}: {1} != {2}\n"; + // builder.AppendFormat(format, stat, originalValue, resultValue); + // } + + // builder.AppendLine("Pro Guitar:"); + // FormatStat("Stat", original.Stat, result.Stat); + // YargLogger.LogDebug(builder.ToString()); + // builder.Clear(); + // } + + // return original.Stat == result.Stat; + // } + + private static bool IsInstrumentPassResult(ProKeysStats original, ProKeysStats result) + { + if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + { + using var builder = new Utf16ValueStringBuilder(true); + + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); + } + + builder.AppendLine("Guitar:"); + FormatStat("Overhits", original.Overhits, result.Overhits); + FormatStat("Fat fingers ignored", original.FatFingersIgnored, result.FatFingersIgnored); + YargLogger.LogDebug(builder.ToString()); + builder.Clear(); + } + + return original.Overhits == result.Overhits && + original.FatFingersIgnored == result.FatFingersIgnored; + } + + // ReSharper restore CompareOfFloatsByEqualityOperator } } \ No newline at end of file From 5c3df0a0922459e7f95a77dcf17e4d5e96fe95fa Mon Sep 17 00:00:00 2001 From: Purplo <33099317+Purplo-cf@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:19:01 -0700 Subject: [PATCH 11/34] Changed engine countdown events to be purely time-based (#218) --- YARG.Core/Chart/Events/ChartEvent.cs | 3 - YARG.Core/Chart/Events/WaitCountdown.cs | 49 +---------- YARG.Core/Engine/BaseEngine.Generic.cs | 104 ++++++------------------ 3 files changed, 28 insertions(+), 128 deletions(-) diff --git a/YARG.Core/Chart/Events/ChartEvent.cs b/YARG.Core/Chart/Events/ChartEvent.cs index 0d0ee22fc..2eadfae1e 100644 --- a/YARG.Core/Chart/Events/ChartEvent.cs +++ b/YARG.Core/Chart/Events/ChartEvent.cs @@ -13,9 +13,6 @@ public abstract class ChartEvent public uint TickLength { get; set; } public uint TickEnd => Tick + TickLength; - // For subclasses that set the base properties through other parameters - public ChartEvent() {} - public ChartEvent(double time, double timeLength, uint tick, uint tickLength) { Time = time; diff --git a/YARG.Core/Chart/Events/WaitCountdown.cs b/YARG.Core/Chart/Events/WaitCountdown.cs index 4486ad293..f33feb40a 100644 --- a/YARG.Core/Chart/Events/WaitCountdown.cs +++ b/YARG.Core/Chart/Events/WaitCountdown.cs @@ -1,59 +1,18 @@ using System.Collections.Generic; +using YARG.Core.Logging; namespace YARG.Core.Chart { public class WaitCountdown : ChartEvent { public const float MIN_SECONDS = 9; - public const uint MIN_MEASURES = 4; - public const float MIN_MEASURE_LENGTH = 1; - public const float FADE_ANIM_LENGTH = 0.45f; - public const int END_COUNTDOWN_MEASURE = 1; - - public int TotalMeasures => _measureBeatlines.Count; + public const float END_COUNTDOWN_SECOND = 1f; //The time where the countdown should start fading out and overstrums will break combo again - public double DeactivateTime => _measureBeatlines[^(END_COUNTDOWN_MEASURE + 1)].Time; - public bool IsActive => MeasuresLeft > END_COUNTDOWN_MEASURE; - - private List _measureBeatlines; - - public int MeasuresLeft {get; private set; } + public double DeactivateTime => TimeEnd - END_COUNTDOWN_SECOND; - public WaitCountdown(List measureBeatlines) + public WaitCountdown(double time, double timeLength, uint tick, uint tickLength) : base(time, timeLength, tick, tickLength) { - _measureBeatlines = measureBeatlines; - - var firstCountdownMeasure = measureBeatlines[0]; - var lastCountdownMeasure = measureBeatlines[^1]; - - Time = firstCountdownMeasure.Time; - Tick = firstCountdownMeasure.Tick; - TimeLength = lastCountdownMeasure.Time - Time; - TickLength = lastCountdownMeasure.Tick - Tick; - - MeasuresLeft = TotalMeasures; - } - - public int CalculateMeasuresLeft(uint currentTick) - { - int newMeasuresLeft; - if (currentTick >= TickEnd) - { - newMeasuresLeft = 0; - } - else if (currentTick < Tick) - { - newMeasuresLeft = TotalMeasures; - } - else - { - newMeasuresLeft = TotalMeasures - _measureBeatlines.GetIndexOfNext(currentTick); - } - - MeasuresLeft = newMeasuresLeft; - - return newMeasuresLeft; } } } \ No newline at end of file diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index d2759d86d..e1e2c626b 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -38,7 +38,7 @@ public abstract class BaseEngine : BaseE public delegate void SustainEndEvent(TNoteType note, double timeEnded, bool finished); - public delegate void CountdownChangeEvent(int measuresLeft, double countdownLength, double endTime); + public delegate void CountdownChangeEvent(double countdownLength, double endTime); public NoteHitEvent? OnNoteHit; public NoteMissedEvent? OnNoteMissed; @@ -276,28 +276,26 @@ protected override void UpdateTimeVariables(double time) if (time >= currentCountdown.Time) { - if (!IsWaitCountdownActive && time < currentCountdown.DeactivateTime) + if (time < currentCountdown.DeactivateTime) { - // Entered new countdown window - IsWaitCountdownActive = true; - YargLogger.LogFormatTrace("Countdown {0} activated at time {1}. Expected time: {2}", CurrentWaitCountdownIndex, time, currentCountdown.Time); - } + // This countdown should be displayed onscreen + if (!IsWaitCountdownActive) + { + // Entered new countdown window + IsWaitCountdownActive = true; + YargLogger.LogFormatTrace("Countdown {0} activated at time {1}. Expected time: {2}", CurrentWaitCountdownIndex, time, currentCountdown.Time); + } - if (time <= currentCountdown.DeactivateTime + WaitCountdown.FADE_ANIM_LENGTH) + UpdateCountdown(currentCountdown.TimeLength, currentCountdown.TimeEnd); + } + else { - // This countdown is currently displayed onscreen - int newMeasuresLeft = currentCountdown.CalculateMeasuresLeft(CurrentTick); - - if (IsWaitCountdownActive && !currentCountdown.IsActive) + if (IsWaitCountdownActive) { IsWaitCountdownActive = false; YargLogger.LogFormatTrace("Countdown {0} deactivated at time {1}. Expected time: {2}", CurrentWaitCountdownIndex, time, currentCountdown.DeactivateTime); } - UpdateCountdown(newMeasuresLeft, currentCountdown.TimeLength, currentCountdown.TimeEnd); - } - else - { CurrentWaitCountdownIndex++; } } @@ -768,9 +766,9 @@ protected void RebaseSustains(uint baseTick) } } - protected void UpdateCountdown(int measuresLeft, double countdownLength, double endTime) + protected void UpdateCountdown(double countdownLength, double endTime) { - OnCountdownChange?.Invoke(measuresLeft, countdownLength, endTime); + OnCountdownChange?.Invoke(countdownLength, endTime); } public sealed override (double FrontEnd, double BackEnd) CalculateHitWindow() @@ -919,83 +917,29 @@ private List GetSoloSections() protected void GetWaitCountdowns(List notes) { - var allMeasureBeatLines = SyncTrack.Beatlines.Where(x => x.Type == BeatlineType.Measure).ToList(); - WaitCountdowns = new List(); for (int i = 0; i < notes.Count; i++) { // Compare the note at the current index against the previous note - // Create a countdown if the distance between the notes is > 10s - Note noteOne; - - uint noteOneTickEnd = 0; double noteOneTimeEnd = 0; + uint noteOneTickEnd = 0; if (i > 0) { - noteOne = notes[i-1]; - noteOneTickEnd = noteOne.TickEnd; + Note noteOne = notes[i-1]; noteOneTimeEnd = noteOne.TimeEnd; + noteOneTickEnd = noteOne.TickEnd; } Note noteTwo = notes[i]; - double noteTwoTime = noteTwo.Time; - if (noteTwoTime - noteOneTimeEnd >= WaitCountdown.MIN_SECONDS) + if (noteTwo.Time - noteOneTimeEnd >= WaitCountdown.MIN_SECONDS) { - uint noteTwoTick = noteTwo.Tick; - - // Determine the total number of measures that will pass during this countdown - List beatlinesThisCountdown = new(); - - // Countdown should start at end of the first note if it's directly on a measure line - // Otherwise it should start at the beginning of the next measure - - // Increasing measure index if there's no more measures causes an exception - // Temporary fix by adding a check for the last measure - // Affects 1/1 time signatures - int curMeasureIndex = allMeasureBeatLines.GetIndexOfPrevious(noteOneTickEnd); - if (allMeasureBeatLines[curMeasureIndex].Tick < noteOneTickEnd - && curMeasureIndex + 1 < allMeasureBeatLines.Count) - { - curMeasureIndex++; - } - - var curMeasureline = allMeasureBeatLines[curMeasureIndex]; - while (curMeasureline.Tick <= noteTwoTick) - { - // Skip counting on measures that are too close together - if (beatlinesThisCountdown.Count == 0 || - curMeasureline.Time - beatlinesThisCountdown.Last().Time >= WaitCountdown.MIN_MEASURE_LENGTH) - { - beatlinesThisCountdown.Add(curMeasureline); - } - - curMeasureIndex++; - - if (curMeasureIndex >= allMeasureBeatLines.Count) - { - break; - } - - curMeasureline = allMeasureBeatLines[curMeasureIndex]; - } - - // Prevent showing countdowns < 4 measures at low BPMs - int countdownTotalMeasures = beatlinesThisCountdown.Count; - if (countdownTotalMeasures >= WaitCountdown.MIN_MEASURES) - { - // Create a WaitCountdown instance to reference at runtime - var newCountdown = new WaitCountdown(beatlinesThisCountdown); + // Distance between these two notes is over the threshold + // Create a WaitCountdown instance to reference at runtime + var newCountdown = new WaitCountdown(noteOneTimeEnd, noteTwo.Time - noteOneTimeEnd, noteOneTickEnd, noteTwo.Tick - noteOneTickEnd); - WaitCountdowns.Add(newCountdown); - YargLogger.LogFormatTrace("Created a WaitCountdown at time {0} of {1} measures and {2} seconds in length", - newCountdown.Time, countdownTotalMeasures, beatlinesThisCountdown[^1].Time - noteOneTimeEnd); - } - else - { - YargLogger.LogFormatTrace("Did not create a WaitCountdown at time {0} of {1} seconds in length because it was only {2} measures long", - noteOneTimeEnd, beatlinesThisCountdown[^1].Time - noteOneTimeEnd, countdownTotalMeasures); - } + WaitCountdowns.Add(newCountdown); + YargLogger.LogFormatTrace("Created a WaitCountdown at time {0} of {1} seconds in length", newCountdown.Time, newCountdown.TimeLength); } } } From 5193bb01e4eb7402fab793c2ae6eaae4f93e7d32 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Tue, 29 Oct 2024 02:13:37 +0000 Subject: [PATCH 12/34] Handle missing a SoloEnd note before the solo has started --- YARG.Core/Engine/BaseEngine.Generic.cs | 14 +++++++++++--- YARG.Core/Engine/Drums/DrumsEngine.cs | 20 +++++++++++++++++--- YARG.Core/Engine/Guitar/GuitarEngine.cs | 21 +++++++++++++++++---- YARG.Core/Engine/ProKeys/ProKeysEngine.cs | 20 +++++++++++++++++--- YARG.Core/Engine/SoloSection.cs | 9 +++++++-- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index e1e2c626b..47d6cdd78 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -874,7 +874,7 @@ private List GetSoloSections() { var soloSections = new List(); - if (Notes.Count > 0 && Notes[0].IsSolo) + if (Notes.Count > 0 && Notes[0] is { IsSolo: true, IsSoloEnd: false }) { Notes[0].ActivateFlag(NoteFlags.SoloStart); } @@ -894,8 +894,16 @@ private List GetSoloSections() // note is a SoloStart - // Try to find a solo end int soloNoteCount = GetNumberOfNotes(start); + + // 1 note solo + if (start.IsSoloEnd) + { + soloSections.Add(new SoloSection(start.Tick, start.Tick, soloNoteCount)); + continue; + } + + // Try to find a solo end for (int j = i + 1; j < Notes.Count; j++) { var end = Notes[j]; @@ -904,7 +912,7 @@ private List GetSoloSections() if (!end.IsSoloEnd) continue; - soloSections.Add(new SoloSection(soloNoteCount)); + soloSections.Add(new SoloSection(start.Tick, end.Tick, soloNoteCount)); // Move i to the end of the solo section i = j; diff --git a/YARG.Core/Engine/Drums/DrumsEngine.cs b/YARG.Core/Engine/Drums/DrumsEngine.cs index fb7534fb3..2a0349552 100644 --- a/YARG.Core/Engine/Drums/DrumsEngine.cs +++ b/YARG.Core/Engine/Drums/DrumsEngine.cs @@ -258,12 +258,26 @@ protected override void MissNote(DrumNote note) StripStarPower(note); } - if (note.IsSoloEnd && note.ParentOrSelf.WasFullyHitOrMissed()) + if (note is { IsSoloStart: true, IsSoloEnd: true } && note.ParentOrSelf.WasFullyHitOrMissed()) + { + // While a solo is active, end the current solo and immediately start the next. + if (IsSoloActive) + { + EndSolo(); + StartSolo(); + } + else + { + // If no solo is currently active, start and immediately end the solo. + StartSolo(); + EndSolo(); + } + } + else if (note.IsSoloEnd && note.ParentOrSelf.WasFullyHitOrMissed()) { EndSolo(); } - - if (note.IsSoloStart) + else if (note.IsSoloStart) { StartSolo(); } diff --git a/YARG.Core/Engine/Guitar/GuitarEngine.cs b/YARG.Core/Engine/Guitar/GuitarEngine.cs index 06c7fac59..23c6e501f 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngine.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngine.cs @@ -273,12 +273,25 @@ protected override void MissNote(GuitarNote note) StripStarPower(note); } - if (note.IsSoloEnd) + // Solo has the start and end flag + if(note is { IsSoloStart: true, IsSoloEnd: true }) + { + // While a solo is active, end the current solo and immediately start the next. + if (IsSoloActive) + { + EndSolo(); + StartSolo(); + } + else + { + // If no solo is currently active, start and immediately end the solo. + StartSolo(); + EndSolo(); + } + } else if(note.IsSoloEnd) { EndSolo(); - } - - if (note.IsSoloStart) + } else if (note.IsSoloStart) { StartSolo(); } diff --git a/YARG.Core/Engine/ProKeys/ProKeysEngine.cs b/YARG.Core/Engine/ProKeys/ProKeysEngine.cs index ff7b4f5a5..0a7bffa70 100644 --- a/YARG.Core/Engine/ProKeys/ProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/ProKeysEngine.cs @@ -262,12 +262,26 @@ protected override void MissNote(ProKeysNote note) StripStarPower(note); } - if (note.IsSoloEnd && note.ParentOrSelf.WasFullyHitOrMissed()) + if (note is { IsSoloStart: true, IsSoloEnd: true } && note.ParentOrSelf.WasFullyHitOrMissed()) + { + // While a solo is active, end the current solo and immediately start the next. + if (IsSoloActive) + { + EndSolo(); + StartSolo(); + } + else + { + // If no solo is currently active, start and immediately end the solo. + StartSolo(); + EndSolo(); + } + } + else if (note.IsSoloEnd && note.ParentOrSelf.WasFullyHitOrMissed()) { EndSolo(); } - - if (note.IsSoloStart) + else if (note.IsSoloStart) { StartSolo(); } diff --git a/YARG.Core/Engine/SoloSection.cs b/YARG.Core/Engine/SoloSection.cs index a5d18324f..3f1229db0 100644 --- a/YARG.Core/Engine/SoloSection.cs +++ b/YARG.Core/Engine/SoloSection.cs @@ -6,11 +6,16 @@ public class SoloSection public int NoteCount { get; } public int NotesHit { get; set; } - + public int SoloBonus { get; set; } - public SoloSection(int noteCount) + public uint StartTick { get; private set; } + public uint EndTick { get; private set; } + + public SoloSection(uint start, uint end, int noteCount) { + StartTick = start; + EndTick = end; NoteCount = noteCount; } From 527f1cd357b4bc354aaa6f40aefe7560ced2ad94 Mon Sep 17 00:00:00 2001 From: sonicfind Date: Tue, 29 Oct 2024 17:04:23 -0500 Subject: [PATCH 13/34] More concise `GetSoloSections()` --- YARG.Core/Engine/BaseEngine.Generic.cs | 44 ++++++++------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index 47d6cdd78..6e2786ed0 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -886,37 +886,21 @@ private List GetSoloSections() for (int i = 0; i < Notes.Count; i++) { - var start = Notes[i]; - if (!start.IsSoloStart) + var curr = Notes[i]; + if (curr.IsSoloStart) { - continue; - } - - // note is a SoloStart - - int soloNoteCount = GetNumberOfNotes(start); - - // 1 note solo - if (start.IsSoloEnd) - { - soloSections.Add(new SoloSection(start.Tick, start.Tick, soloNoteCount)); - continue; - } - - // Try to find a solo end - for (int j = i + 1; j < Notes.Count; j++) - { - var end = Notes[j]; - - soloNoteCount += GetNumberOfNotes(end); - - if (!end.IsSoloEnd) continue; - - soloSections.Add(new SoloSection(start.Tick, end.Tick, soloNoteCount)); - - // Move i to the end of the solo section - i = j; - break; + int soloNoteCount = 0; + uint start = curr.Tick; + while (true) + { + soloNoteCount += GetNumberOfNotes(curr); + if (curr.IsSoloEnd || i + 1 == Notes.Count) + { + break; + } + curr = Notes[++i]; + } + soloSections.Add(new SoloSection(start, curr.Tick, soloNoteCount)); } } From c31f12332f940752c00a12b4251e8d3b89eef350 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Tue, 26 Nov 2024 23:43:23 +0000 Subject: [PATCH 14/34] Simulate replay to Replay Length time --- ReplayCli/Cli.SimulateFps.cs | 2 +- ReplayCli/Cli.Verify.cs | 2 +- YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ReplayCli/Cli.SimulateFps.cs b/ReplayCli/Cli.SimulateFps.cs index 0ccc27ea7..0dca6efc2 100644 --- a/ReplayCli/Cli.SimulateFps.cs +++ b/ReplayCli/Cli.SimulateFps.cs @@ -24,7 +24,7 @@ private bool RunSimulateFps() { double fps = i * 4 + 1; - var analyzerResults = ReplayAnalyzer.AnalyzeReplay(chart, _replayData, fps); + var analyzerResults = ReplayAnalyzer.AnalyzeReplay(chart, _replayInfo, _replayData, fps); long bandScore = analyzerResults.Sum(x => (long) x.ResultStats.TotalScore); if (scores.TryGetValue(bandScore, out int value)) diff --git a/ReplayCli/Cli.Verify.cs b/ReplayCli/Cli.Verify.cs index a525b53a9..1d90b645c 100644 --- a/ReplayCli/Cli.Verify.cs +++ b/ReplayCli/Cli.Verify.cs @@ -18,7 +18,7 @@ private bool RunVerify() Console.WriteLine("Analyzing replay..."); - var results = ReplayAnalyzer.AnalyzeReplay(chart, _replayData); + var results = ReplayAnalyzer.AnalyzeReplay(chart, _replayInfo, _replayData); Console.WriteLine("Done!\n"); diff --git a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs index 4bd15ca62..a26587cca 100644 --- a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs +++ b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs @@ -20,25 +20,29 @@ namespace YARG.Core.Replays.Analyzer public class ReplayAnalyzer { private readonly SongChart _chart; - private readonly ReplayData _replay; + + private readonly ReplayInfo _replayInfo; + private readonly ReplayData _replayData; private readonly double _fps; private readonly bool _doFrameUpdates; private readonly Random _random = new(); - public ReplayAnalyzer(SongChart chart, ReplayData replay, double fps) + public ReplayAnalyzer(SongChart chart, ReplayInfo replayInfo, ReplayData replayData, double fps) { _chart = chart; - _replay = replay; + + _replayInfo = replayInfo; + _replayData = replayData; _fps = fps; _doFrameUpdates = _fps > 0; } - public static AnalysisResult[] AnalyzeReplay(SongChart chart, ReplayData replay, double fps = 0) + public static AnalysisResult[] AnalyzeReplay(SongChart chart, ReplayInfo info, ReplayData data, double fps = 0) { - var analyzer = new ReplayAnalyzer(chart, replay, fps); + var analyzer = new ReplayAnalyzer(chart, info, data, fps); return analyzer.Analyze(); } @@ -132,11 +136,11 @@ void AppendStatDifference(string name, T frameStat, T resultStat) private AnalysisResult[] Analyze() { - var results = new AnalysisResult[_replay.Frames.Length]; + var results = new AnalysisResult[_replayData.Frames.Length]; for (int i = 0; i < results.Length; i++) { - var frame = _replay.Frames[i]; + var frame = _replayData.Frames[i]; var result = RunFrame(frame); results[i] = result; @@ -151,7 +155,7 @@ private AnalysisResult RunFrame(ReplayFrame frame) engine.SetSpeed(frame.EngineParameters.SongSpeed); engine.Reset(); - double maxTime = _chart.GetEndTime(); + double maxTime = _replayInfo.ReplayLength; if (frame.Inputs.Length > 0) { double last = frame.Inputs[^1].Time; From c58e81830bf3f5204dca1ba52dcfb1734a280a23 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Wed, 27 Nov 2024 00:31:46 +0000 Subject: [PATCH 15/34] Calculate Time In SP every update and store a base time --- YARG.Core/Engine/BaseEngine.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index 94a64a6c5..a40c25179 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -96,6 +96,8 @@ public abstract class BaseEngine public double StarPowerActivationTime { get; protected set; } public double StarPowerEndTime { get; protected set; } + public double BaseTimeInStarPower { get; protected set; } + public readonly struct EngineFrameUpdate { public EngineFrameUpdate(double time, string reason) @@ -499,8 +501,13 @@ protected void ReleaseStarPower() { YargLogger.LogFormatTrace("Star Power ended at {0} (tick: {1})", CurrentTime, StarPowerTickPosition); + BaseStats.IsStarPowerActive = false; - BaseStats.TimeInStarPower += CurrentTime - StarPowerActivationTime; + + double spTimeDelta = CurrentTime - StarPowerActivationTime; + BaseStats.TimeInStarPower = spTimeDelta + BaseTimeInStarPower; + + BaseTimeInStarPower = BaseStats.TimeInStarPower; RebaseProgressValues(CurrentTick); @@ -551,6 +558,8 @@ protected virtual void UpdateStarPower() if (BaseStats.IsStarPowerActive) { DrainStarPower(StarPowerTickPosition - PreviousStarPowerTickPosition); + double spTimeDelta = CurrentTime - StarPowerActivationTime; + BaseStats.TimeInStarPower = spTimeDelta + BaseTimeInStarPower; if (BaseStats.StarPowerTickAmount <= 0) { From 17c6ef167aea9771b2870ea25524fb22a242cedd Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Thu, 28 Nov 2024 00:59:43 +0000 Subject: [PATCH 16/34] Return string from analyzer stat diff instead of logging --- YARG.Core/Replays/Analyzer/AnalysisResult.cs | 5 +- YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs | 242 +++++++++---------- 2 files changed, 114 insertions(+), 133 deletions(-) diff --git a/YARG.Core/Replays/Analyzer/AnalysisResult.cs b/YARG.Core/Replays/Analyzer/AnalysisResult.cs index 241f7edda..3ea3c0772 100644 --- a/YARG.Core/Replays/Analyzer/AnalysisResult.cs +++ b/YARG.Core/Replays/Analyzer/AnalysisResult.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using YARG.Core.Engine; +using YARG.Core.Engine; namespace YARG.Core.Replays.Analyzer { @@ -13,5 +12,7 @@ public struct AnalysisResult public BaseStats ResultStats; public int ScoreDifference; + + public string StatLog; } } \ No newline at end of file diff --git a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs index a26587cca..11b6d0a8b 100644 --- a/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs +++ b/YARG.Core/Replays/Analyzer/ReplayAnalyzer.cs @@ -19,7 +19,7 @@ namespace YARG.Core.Replays.Analyzer { public class ReplayAnalyzer { - private readonly SongChart _chart; + private readonly SongChart _chart; private readonly ReplayInfo _replayInfo; private readonly ReplayData _replayData; @@ -131,6 +131,7 @@ void AppendStatDifference(string name, T frameStat, T resultStat) break; } } + return sb.ToString(); } @@ -164,7 +165,6 @@ private AnalysisResult RunFrame(ReplayFrame frame) maxTime = last; } } - maxTime += 2; if (!_doFrameUpdates) { @@ -200,7 +200,7 @@ private AnalysisResult RunFrame(ReplayFrame frame) } } - bool passed = IsPassResult(frame.Stats, engine.BaseStats); + bool passed = IsPassResult(frame.Stats, engine.BaseStats, out string log); return new AnalysisResult { @@ -208,6 +208,7 @@ private AnalysisResult RunFrame(ReplayFrame frame) Frame = frame, OriginalStats = frame.Stats, ResultStats = engine.BaseStats, + StatLog = log, }; } @@ -335,63 +336,62 @@ private List GenerateFrameTimes(double from, double to) // ReSharper disable CompareOfFloatsByEqualityOperator - private static bool IsPassResult(BaseStats original, BaseStats result) + private static bool IsPassResult(BaseStats original, BaseStats result, out string log) { // For easier maintenance/reading, manually check log level and // use a string builder instead of LogFormat methods - if (YargLogger.MinimumLogLevel <= LogLevel.Debug) - { - using var builder = new Utf16ValueStringBuilder(true); - // This helper is copied to the instrument-specific methods - // because passing a `using` variable by reference is disallowed, - // and working around that is annoying - void FormatStat(string stat, T originalValue, T resultValue) - where T : IEquatable - { - string format = originalValue.Equals(resultValue) - ? "- {0}: {1} == {2}\n" - : "- {0}: {1} != {2}\n"; - builder.AppendFormat(format, stat, originalValue, resultValue); - } + var builder = new Utf16ValueStringBuilder(true); - // Commented stats aren't serialized - - builder.AppendLine("Scoring:"); - FormatStat("Committed score", original.CommittedScore, result.CommittedScore); - FormatStat("Pending score", original.PendingScore, result.PendingScore); - FormatStat("Score from notes", original.NoteScore, result.NoteScore); - FormatStat("Score from sustains", original.SustainScore, result.SustainScore); - FormatStat("Score from multipliers", original.MultiplierScore, result.MultiplierScore); - FormatStat("Score from solos", original.SoloBonuses, result.SoloBonuses); - FormatStat("Score from SP", original.StarPowerScore, result.StarPowerScore); - // FormatStat("Stars", original.Stars, result.Stars); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); - - builder.AppendLine("Performance:"); - FormatStat("Notes hit", original.NotesHit, result.NotesHit); - FormatStat("Notes missed", original.NotesMissed, result.NotesMissed); - FormatStat("Combo", original.Combo, result.Combo); - FormatStat("Max combo", original.MaxCombo, result.MaxCombo); - FormatStat("Multiplier", original.ScoreMultiplier, result.ScoreMultiplier); - FormatStat("Hit percent", original.Percent, result.Percent); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); - - builder.AppendLine("Star Power:"); - FormatStat("Phrases hit", original.StarPowerPhrasesHit, result.StarPowerPhrasesHit); - FormatStat("Phrases missed", original.StarPowerPhrasesMissed, result.StarPowerPhrasesMissed); - FormatStat("Total ticks earned", original.TotalStarPowerTicks, result.TotalStarPowerTicks); - FormatStat("Remaining ticks", original.StarPowerTickAmount, result.StarPowerTickAmount); - FormatStat("Ticks from whammy", original.StarPowerWhammyTicks, result.StarPowerWhammyTicks); - FormatStat("Time in SP", original.TimeInStarPower, result.TimeInStarPower); - // FormatStat("Activation count", original.StarPowerActivationCount, result.StarPowerActivationCount); - // FormatStat("Total bars filled", original.TotalStarPowerBarsFilled, result.TotalStarPowerBarsFilled); - FormatStat("Ended with SP active", original.IsStarPowerActive, result.IsStarPowerActive); - YargLogger.LogDebug(builder.ToString()); + // This helper is copied to the instrument-specific methods + // because passing a `using` variable by reference is disallowed, + // and working around that is annoying + void FormatStat(string stat, T originalValue, T resultValue) + where T : IEquatable + { + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); } + // Commented stats aren't serialized + + builder.AppendLine("Scoring:"); + FormatStat("Committed score", original.CommittedScore, result.CommittedScore); + FormatStat("Pending score", original.PendingScore, result.PendingScore); + FormatStat("Score from notes", original.NoteScore, result.NoteScore); + FormatStat("Score from sustains", original.SustainScore, result.SustainScore); + FormatStat("Score from multipliers", original.MultiplierScore, result.MultiplierScore); + FormatStat("Score from solos", original.SoloBonuses, result.SoloBonuses); + FormatStat("Score from SP", original.StarPowerScore, result.StarPowerScore); + // FormatStat("Stars", original.Stars, result.Stars); + + builder.AppendLine(); + + builder.AppendLine("Performance:"); + FormatStat("Notes hit", original.NotesHit, result.NotesHit); + FormatStat("Notes missed", original.NotesMissed, result.NotesMissed); + FormatStat("Combo", original.Combo, result.Combo); + FormatStat("Max combo", original.MaxCombo, result.MaxCombo); + FormatStat("Multiplier", original.ScoreMultiplier, result.ScoreMultiplier); + FormatStat("Hit percent", original.Percent, result.Percent); + + builder.AppendLine(); + + builder.AppendLine("Star Power:"); + FormatStat("Phrases hit", original.StarPowerPhrasesHit, result.StarPowerPhrasesHit); + FormatStat("Phrases missed", original.StarPowerPhrasesMissed, result.StarPowerPhrasesMissed); + FormatStat("Total ticks earned", original.TotalStarPowerTicks, result.TotalStarPowerTicks); + FormatStat("Remaining ticks", original.StarPowerTickAmount, result.StarPowerTickAmount); + FormatStat("Ticks from whammy", original.StarPowerWhammyTicks, result.StarPowerWhammyTicks); + FormatStat("Time in SP", original.TimeInStarPower, result.TimeInStarPower); + // FormatStat("Activation count", original.StarPowerActivationCount, result.StarPowerActivationCount); + // FormatStat("Total bars filled", original.TotalStarPowerBarsFilled, result.TotalStarPowerBarsFilled); + FormatStat("Ended with SP active", original.IsStarPowerActive, result.IsStarPowerActive); + + builder.AppendLine(); + bool scoringPass = original.CommittedScore == result.CommittedScore && original.PendingScore == result.PendingScore && @@ -400,7 +400,7 @@ void FormatStat(string stat, T originalValue, T resultValue) original.MultiplierScore == result.MultiplierScore && original.SoloBonuses == result.SoloBonuses && original.StarPowerScore == result.StarPowerScore; // && - // original.Stars == result.Stars; + // original.Stars == result.Stars; bool performancePass = original.NotesHit == result.NotesHit && @@ -428,42 +428,45 @@ void FormatStat(string stat, T originalValue, T resultValue) switch (original, result) { case (GuitarStats guitar1, GuitarStats guitar2): - instrumentPass = IsInstrumentPassResult(guitar1, guitar2); + instrumentPass = IsInstrumentPassResult(guitar1, guitar2, ref builder); break; case (DrumsStats drums1, DrumsStats drums2): - instrumentPass = IsInstrumentPassResult(drums1, drums2); + instrumentPass = IsInstrumentPassResult(drums1, drums2, ref builder); break; case (VocalsStats vox1, VocalsStats vox2): - instrumentPass = IsInstrumentPassResult(vox1, vox2); + instrumentPass = IsInstrumentPassResult(vox1, vox2, ref builder); break; // case (ProGuitarStats pg1, ProGuitarStats pg2): - // instrumentPass = IsInstrumentPassResult(pg1, pg2); + // instrumentPass = IsInstrumentPassResult(pg1, pg2, ref builder); // break; case (ProKeysStats pk1, ProKeysStats pk2): - instrumentPass = IsInstrumentPassResult(pk1, pk2); + instrumentPass = IsInstrumentPassResult(pk1, pk2, ref builder); break; default: YargLogger.Assert(original.GetType() == result.GetType()); - YargLogger.LogFormatDebug("Instrument-specific validation not yet implemented for {0}", original.GetType()); + YargLogger.LogFormatDebug("Instrument-specific validation not yet implemented for {0}", + original.GetType()); instrumentPass = true; break; } + log = builder.ToString(); + builder.Dispose(); + return generalPass && instrumentPass; } - private static bool IsInstrumentPassResult(GuitarStats original, GuitarStats result) + private static bool IsInstrumentPassResult(GuitarStats original, GuitarStats result, + ref Utf16ValueStringBuilder builder) { if (YargLogger.MinimumLogLevel <= LogLevel.Debug) { - using var builder = new Utf16ValueStringBuilder(true); - - void FormatStat(string stat, T originalValue, T resultValue) + void FormatStat(string stat, T originalValue, T resultValue, ref Utf16ValueStringBuilder builder) where T : IEquatable { string format = originalValue.Equals(resultValue) @@ -473,11 +476,9 @@ void FormatStat(string stat, T originalValue, T resultValue) } builder.AppendLine("Guitar:"); - FormatStat("Overstrums", original.Overstrums, result.Overstrums); - FormatStat("Ghost inputs", original.GhostInputs, result.GhostInputs); - FormatStat("HOPOs strummed", original.HoposStrummed, result.HoposStrummed); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); + FormatStat("Overstrums", original.Overstrums, result.Overstrums, ref builder); + FormatStat("Ghost inputs", original.GhostInputs, result.GhostInputs, ref builder); + FormatStat("HOPOs strummed", original.HoposStrummed, result.HoposStrummed, ref builder); } return original.Overstrums == result.Overstrums && @@ -485,36 +486,29 @@ void FormatStat(string stat, T originalValue, T resultValue) original.HoposStrummed == result.HoposStrummed; } - private static bool IsInstrumentPassResult(DrumsStats original, DrumsStats result) + private static bool IsInstrumentPassResult(DrumsStats original, DrumsStats result, ref Utf16ValueStringBuilder builder) { - if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + void FormatStat(string stat, T originalValue, T resultValue, ref Utf16ValueStringBuilder builder) + where T : IEquatable { - using var builder = new Utf16ValueStringBuilder(true); - - void FormatStat(string stat, T originalValue, T resultValue) - where T : IEquatable - { - string format = originalValue.Equals(resultValue) - ? "- {0}: {1} == {2}\n" - : "- {0}: {1} != {2}\n"; - builder.AppendFormat(format, stat, originalValue, resultValue); - } - - builder.AppendLine("Drums:"); - FormatStat("Overhits", original.Overhits, result.Overhits); - FormatStat("Ghosts hit correctly", original.GhostsHit, result.GhostsHit); - FormatStat("Ghosts hit incorrectly", - original.TotalGhosts - original.GhostsHit, - result.TotalGhosts - result.GhostsHit); - FormatStat("Accents hit correctly", original.AccentsHit, result.AccentsHit); - FormatStat("Accents hit incorrectly", - original.TotalAccents - original.AccentsHit, - result.TotalAccents - result.AccentsHit); - FormatStat("Score from dynamics", original.DynamicsBonus, result.DynamicsBonus); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); } + builder.AppendLine("Drums:"); + FormatStat("Overhits", original.Overhits, result.Overhits, ref builder); + FormatStat("Ghosts hit correctly", original.GhostsHit, result.GhostsHit, ref builder); + FormatStat("Ghosts hit incorrectly", + original.TotalGhosts - original.GhostsHit, + result.TotalGhosts - result.GhostsHit, ref builder); + FormatStat("Accents hit correctly", original.AccentsHit, result.AccentsHit, ref builder); + FormatStat("Accents hit incorrectly", + original.TotalAccents - original.AccentsHit, + result.TotalAccents - result.AccentsHit, ref builder); + FormatStat("Score from dynamics", original.DynamicsBonus, result.DynamicsBonus, ref builder); + return original.Overhits == result.Overhits && original.GhostsHit == result.GhostsHit && original.TotalGhosts == result.TotalGhosts && @@ -523,28 +517,21 @@ void FormatStat(string stat, T originalValue, T resultValue) original.DynamicsBonus == result.DynamicsBonus; } - private static bool IsInstrumentPassResult(VocalsStats original, VocalsStats result) + private static bool IsInstrumentPassResult(VocalsStats original, VocalsStats result, ref Utf16ValueStringBuilder builder) { - if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + void FormatStat(string stat, T originalValue, T resultValue, ref Utf16ValueStringBuilder builder) + where T : IEquatable { - using var builder = new Utf16ValueStringBuilder(true); - - void FormatStat(string stat, T originalValue, T resultValue) - where T : IEquatable - { - string format = originalValue.Equals(resultValue) - ? "- {0}: {1} == {2}\n" - : "- {0}: {1} != {2}\n"; - builder.AppendFormat(format, stat, originalValue, resultValue); - } - - builder.AppendLine("Vocals:"); - FormatStat("Note ticks hit", original.TicksHit, result.TicksHit); - FormatStat("Note ticks missed", original.TicksMissed, result.TicksMissed); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); } + builder.AppendLine("Vocals:"); + FormatStat("Note ticks hit", original.TicksHit, result.TicksHit, ref builder); + FormatStat("Note ticks missed", original.TicksMissed, result.TicksMissed, ref builder); + return original.TicksHit == result.TicksHit && original.TicksMissed == result.TicksMissed; } @@ -573,28 +560,21 @@ void FormatStat(string stat, T originalValue, T resultValue) // return original.Stat == result.Stat; // } - private static bool IsInstrumentPassResult(ProKeysStats original, ProKeysStats result) + private static bool IsInstrumentPassResult(ProKeysStats original, ProKeysStats result, ref Utf16ValueStringBuilder builder) { - if (YargLogger.MinimumLogLevel <= LogLevel.Debug) + void FormatStat(string stat, T originalValue, T resultValue, ref Utf16ValueStringBuilder builder) + where T : IEquatable { - using var builder = new Utf16ValueStringBuilder(true); - - void FormatStat(string stat, T originalValue, T resultValue) - where T : IEquatable - { - string format = originalValue.Equals(resultValue) - ? "- {0}: {1} == {2}\n" - : "- {0}: {1} != {2}\n"; - builder.AppendFormat(format, stat, originalValue, resultValue); - } - - builder.AppendLine("Guitar:"); - FormatStat("Overhits", original.Overhits, result.Overhits); - FormatStat("Fat fingers ignored", original.FatFingersIgnored, result.FatFingersIgnored); - YargLogger.LogDebug(builder.ToString()); - builder.Clear(); + string format = originalValue.Equals(resultValue) + ? "- {0}: {1} == {2}\n" + : "- {0}: {1} != {2}\n"; + builder.AppendFormat(format, stat, originalValue, resultValue); } + builder.AppendLine("Guitar:"); + FormatStat("Overhits", original.Overhits, result.Overhits, ref builder); + FormatStat("Fat fingers ignored", original.FatFingersIgnored, result.FatFingersIgnored, ref builder); + return original.Overhits == result.Overhits && original.FatFingersIgnored == result.FatFingersIgnored; } From 458aa5b9e44a17cd418d66ab29db94dcca245066 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 29 Nov 2024 00:30:54 +0000 Subject: [PATCH 17/34] Initialize whammy timer in BaseEngine.Generic --- YARG.Core/Engine/BaseEngine.Generic.cs | 4 ++++ YARG.Core/Engine/Guitar/GuitarEngine.cs | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index 6e2786ed0..2ff3efa8e 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -75,6 +75,8 @@ protected BaseEngine(InstrumentDifficulty chart, SyncTrack syncTrack, Notes = Chart.Notes; EngineParameters = engineParameters; + StarPowerWhammyTimer = new EngineTimer(engineParameters.StarPowerWhammyBuffer); + EngineStats = new TEngineStats(); Reset(); @@ -336,6 +338,8 @@ public override void Reset(bool keepCurrentButtons = false) EngineStats.Reset(); + StarPowerWhammyTimer.Disable(); + foreach (var note in Notes) { note.ResetNoteState(); diff --git a/YARG.Core/Engine/Guitar/GuitarEngine.cs b/YARG.Core/Engine/Guitar/GuitarEngine.cs index 23c6e501f..56d9d34da 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngine.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngine.cs @@ -47,7 +47,6 @@ protected GuitarEngine(InstrumentDifficulty chart, SyncTrack syncTra { StrumLeniencyTimer = new EngineTimer(engineParameters.StrumLeniency); HopoLeniencyTimer = new EngineTimer(engineParameters.HopoLeniency); - StarPowerWhammyTimer = new EngineTimer(engineParameters.StarPowerWhammyBuffer); GetWaitCountdowns(Notes); } @@ -96,7 +95,6 @@ public override void Reset(bool keepCurrentButtons = false) StrumLeniencyTimer.Disable(); HopoLeniencyTimer.Disable(); - StarPowerWhammyTimer.Disable(); FrontEndExpireTime = 0; From 0f9bbd247e9e3b9c3217a368bfa0d9eb53aaccb9 Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Fri, 29 Nov 2024 00:33:50 +0000 Subject: [PATCH 18/34] Fix some inconsistent whammy logic --- YARG.Core/Engine/BaseEngine.Generic.cs | 16 ++++++++++++---- YARG.Core/Engine/BaseEngine.cs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index 2ff3efa8e..82df109e0 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -443,13 +443,13 @@ protected virtual void UpdateSustains() { EngineStats.PendingScore = 0; - bool isStarPowerSustainActive = false; + bool isStarPowerSustainActiveRightNow = false; for (int i = 0; i < ActiveSustains.Count; i++) { ref var sustain = ref ActiveSustains[i]; var note = sustain.Note; - isStarPowerSustainActive |= note.IsStarPower; + isStarPowerSustainActiveRightNow |= note.IsStarPower; // If we're close enough to the end of the sustain, finish it // Provides leniency for sustains with no gap (and just in general) @@ -547,9 +547,9 @@ protected virtual void UpdateSustains() UpdateStars(); - if (isStarPowerSustainActive && StarPowerWhammyTimer.IsActive) + if ((WasSpSustainActive || isStarPowerSustainActiveRightNow) && StarPowerWhammyTimer.IsActive) { - var whammyTicks = CurrentTick - LastTick; + var whammyTicks = GetWhammyTicks(isStarPowerSustainActiveRightNow); // Just started whammying, award 1 tick if (!LastWhammyTimerState) @@ -559,16 +559,24 @@ protected virtual void UpdateSustains() GainStarPower(whammyTicks); EngineStats.StarPowerWhammyTicks += whammyTicks; + YargLogger.LogFormatTrace("Gained {0} whammy ticks this update (Total: {1}), {2} sustains active. WasSP: {3}, SP right now: {4}", whammyTicks, EngineStats.StarPowerWhammyTicks, ActiveSustains.Count, WasSpSustainActive, isStarPowerSustainActiveRightNow); } LastWhammyTimerState = StarPowerWhammyTimer.IsActive; + WasSpSustainActive = false; + foreach (var sustain in ActiveSustains) + { + WasSpSustainActive |= sustain.Note.IsStarPower; + } + // Whammy is disabled after sustains are updated. // This is because all the ticks that have accumulated will have been accounted for when it is disabled. // Whereas disabling it before could mean there are some ticks which should have been whammied but weren't. if (StarPowerWhammyTimer.IsActive && StarPowerWhammyTimer.IsExpired(CurrentTime)) { StarPowerWhammyTimer.Disable(); + YargLogger.LogFormatTrace("Disabling whammy timer at {0}", CurrentTime); } } diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index a40c25179..f73f74dd6 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -87,6 +87,11 @@ public abstract class BaseEngine protected bool LastWhammyTimerState; + /// + /// A Star Power Sustain was active in the last update. + /// + protected bool WasSpSustainActive; + public uint StarPowerTickPosition { get; protected set; } public uint PreviousStarPowerTickPosition { get; protected set; } @@ -693,6 +698,17 @@ private double GetStarPowerDrainTickToTime(uint starPowerTick, SyncTrackChange c return change.Time + offset; } + public uint GetWhammyTicks(bool currentSpSustainState) + { + // Just hit the SP sustain this update + if (!WasSpSustainActive && currentSpSustainState) + { + return 1; + } + + return CurrentTick - LastTick; + } + protected virtual void RebaseProgressValues(uint baseTick) { From 34ee620c856df8bec3870db6b7c1a71c7245d62d Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Sat, 30 Nov 2024 01:11:20 +0000 Subject: [PATCH 19/34] Decouple whammy gain from sustains --- YARG.Core/Engine/BaseEngine.Generic.cs | 101 +++++++++++++++---------- YARG.Core/Engine/BaseEngine.cs | 62 ++++----------- 2 files changed, 75 insertions(+), 88 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.Generic.cs b/YARG.Core/Engine/BaseEngine.Generic.cs index 82df109e0..dc5816e94 100644 --- a/YARG.Core/Engine/BaseEngine.Generic.cs +++ b/YARG.Core/Engine/BaseEngine.Generic.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -240,16 +240,6 @@ protected override void GenerateQueuedUpdates(double nextTime) } } } - - if (StarPowerWhammyTimer.IsActive) - { - if (IsTimeBetween(StarPowerWhammyTimer.EndTime, previousTime, nextTime)) - { - YargLogger.LogFormatTrace("Queuing star power whammy end time at {0}", - StarPowerWhammyTimer.EndTime); - QueueUpdateTime(StarPowerWhammyTimer.EndTime, "Star Power Whammy End"); - } - } } protected override void UpdateTimeVariables(double time) @@ -546,10 +536,58 @@ protected virtual void UpdateSustains() } UpdateStars(); + } + + protected virtual void StartSustain(TNoteType note) + { + var sustain = new ActiveSustain(note); + + ActiveSustains.Add(sustain); + + YargLogger.LogFormatTrace("Started sustain at {0} (tick len: {1}, time len: {2})", CurrentTime, note.TickLength, note.TimeLength); + + OnSustainStart?.Invoke(note); + } - if ((WasSpSustainActive || isStarPowerSustainActiveRightNow) && StarPowerWhammyTimer.IsActive) + protected virtual void EndSustain(int sustainIndex, bool dropped, bool isEndOfSustain) + { + var sustain = ActiveSustains[sustainIndex]; + YargLogger.LogFormatTrace("Ended sustain ({0}) at {1} (dropped: {2}, end: {3})", sustain.Note.Tick, CurrentTime, dropped, isEndOfSustain); + ActiveSustains.RemoveAt(sustainIndex); + + OnSustainEnd?.Invoke(sustain.Note, CurrentTime, sustain.HasFinishedScoring); + } + + protected override void UpdateStarPower() + { + PreviousStarPowerTickPosition = StarPowerTickPosition; + StarPowerTickPosition = GetStarPowerDrainTimeToTicks(CurrentTime, CurrentSyncTrackState); + + if (BaseStats.IsStarPowerActive) { - var whammyTicks = GetWhammyTicks(isStarPowerSustainActiveRightNow); + var drain = StarPowerTickPosition - PreviousStarPowerTickPosition; + if ((int) BaseStats.StarPowerTickAmount - drain <= 0) + { + BaseStats.StarPowerTickAmount = 0; + } + else + { + BaseStats.StarPowerTickAmount -= drain; + } + + double spTimeDelta = CurrentTime - StarPowerActivationTime; + BaseStats.TimeInStarPower = spTimeDelta + BaseTimeInStarPower; + } + + bool isStarPowerSustainActive = false; + foreach (var sustain in ActiveSustains) + { + isStarPowerSustainActive |= sustain.Note.IsStarPower; + } + + if (isStarPowerSustainActive && StarPowerWhammyTimer.IsActive) + { + var whammyTicks = CurrentTick - LastTick; // Just started whammying, award 1 tick if (!LastWhammyTimerState) @@ -558,21 +596,22 @@ protected virtual void UpdateSustains() } GainStarPower(whammyTicks); - EngineStats.StarPowerWhammyTicks += whammyTicks; - YargLogger.LogFormatTrace("Gained {0} whammy ticks this update (Total: {1}), {2} sustains active. WasSP: {3}, SP right now: {4}", whammyTicks, EngineStats.StarPowerWhammyTicks, ActiveSustains.Count, WasSpSustainActive, isStarPowerSustainActiveRightNow); + BaseStats.StarPowerWhammyTicks += whammyTicks; + YargLogger.LogFormatTrace("Gained {0} whammy ticks this update (Total: {1}), {2} sustains active. SP right now: {3}", whammyTicks, EngineStats.StarPowerWhammyTicks, ActiveSustains.Count, isStarPowerSustainActive); } LastWhammyTimerState = StarPowerWhammyTimer.IsActive; - WasSpSustainActive = false; - foreach (var sustain in ActiveSustains) + if (BaseStats is { IsStarPowerActive: true, StarPowerTickAmount: 0 }) + { + ReleaseStarPower(); + } + + if (IsStarPowerInputActive && CanStarPowerActivate) { - WasSpSustainActive |= sustain.Note.IsStarPower; + ActivateStarPower(); } - // Whammy is disabled after sustains are updated. - // This is because all the ticks that have accumulated will have been accounted for when it is disabled. - // Whereas disabling it before could mean there are some ticks which should have been whammied but weren't. if (StarPowerWhammyTimer.IsActive && StarPowerWhammyTimer.IsExpired(CurrentTime)) { StarPowerWhammyTimer.Disable(); @@ -580,26 +619,6 @@ protected virtual void UpdateSustains() } } - protected virtual void StartSustain(TNoteType note) - { - var sustain = new ActiveSustain(note); - - ActiveSustains.Add(sustain); - - YargLogger.LogFormatTrace("Started sustain at {0} (tick len: {1}, time len: {2})", CurrentTime, note.TickLength, note.TimeLength); - - OnSustainStart?.Invoke(note); - } - - protected virtual void EndSustain(int sustainIndex, bool dropped, bool isEndOfSustain) - { - var sustain = ActiveSustains[sustainIndex]; - YargLogger.LogFormatTrace("Ended sustain ({0}) at {1} (dropped: {2}, end: {3})", sustain.Note.Tick, CurrentTime, dropped, isEndOfSustain); - ActiveSustains.RemoveAt(sustainIndex); - - OnSustainEnd?.Invoke(sustain.Note, CurrentTime, sustain.HasFinishedScoring); - } - protected void UpdateStars() { // Update which star we're on diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index f73f74dd6..451c9ad6a 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using YARG.Core.Chart; @@ -345,6 +345,16 @@ protected virtual void GenerateQueuedUpdates(double nextTime) YargLogger.LogFormatTrace("Generating queued updates up to {0}", nextTime); var previousTime = CurrentTime; + if (StarPowerWhammyTimer.IsActive) + { + if (IsTimeBetween(StarPowerWhammyTimer.EndTime, previousTime, nextTime)) + { + YargLogger.LogFormatTrace("Queuing star power whammy end time at {0}", + StarPowerWhammyTimer.EndTime); + QueueUpdateTime(StarPowerWhammyTimer.EndTime, "Star Power Whammy End"); + } + } + if (BaseStats.IsStarPowerActive) { if (IsTimeBetween(StarPowerEndTime, previousTime, nextTime)) @@ -533,6 +543,8 @@ protected void GainStarPower(uint ticks) // Add the amount of ticks gained to the total ticks gained BaseStats.TotalStarPowerTicks += BaseStats.StarPowerTickAmount - prevTicks; + YargLogger.LogFormatTrace("Earned {0} ticks of SP at SP tick position {1}, current: {2}, new total: {3}", BaseStats.StarPowerTickAmount - prevTicks, + StarPowerTickPosition, BaseStats.StarPowerTickAmount, BaseStats.TotalStarPowerTicks); BaseStats.TotalStarPowerBarsFilled = (double)BaseStats.TotalStarPowerTicks / TicksPerFullSpBar; if (BaseStats.IsStarPowerActive) @@ -543,40 +555,7 @@ protected void GainStarPower(uint ticks) } } - protected void DrainStarPower(uint starPowerTicks) - { - int newAmount = (int) BaseStats.StarPowerTickAmount - (int) starPowerTicks; - - if (newAmount <= 0) - { - newAmount = 0; - } - - BaseStats.StarPowerTickAmount = (uint) newAmount; - } - - protected virtual void UpdateStarPower() - { - PreviousStarPowerTickPosition = StarPowerTickPosition; - StarPowerTickPosition = GetStarPowerDrainTimeToTicks(CurrentTime, CurrentSyncTrackState); - - if (BaseStats.IsStarPowerActive) - { - DrainStarPower(StarPowerTickPosition - PreviousStarPowerTickPosition); - double spTimeDelta = CurrentTime - StarPowerActivationTime; - BaseStats.TimeInStarPower = spTimeDelta + BaseTimeInStarPower; - - if (BaseStats.StarPowerTickAmount <= 0) - { - ReleaseStarPower(); - } - } - - if (IsStarPowerInputActive && CanStarPowerActivate) - { - ActivateStarPower(); - } - } + protected abstract void UpdateStarPower(); /// /// Calculates the drain to gain ratio for Star Power for a given @@ -634,7 +613,7 @@ private double GetStarPowerDrainTicksToPeriod(double ticks, TempoChange tempo, T return period; } - private uint GetStarPowerDrainTimeToTicks(double time, SyncTrackChange change) + protected uint GetStarPowerDrainTimeToTicks(double time, SyncTrackChange change) { var tempo = change.Tempo; var ts = change.TimeSignature; @@ -698,17 +677,6 @@ private double GetStarPowerDrainTickToTime(uint starPowerTick, SyncTrackChange c return change.Time + offset; } - public uint GetWhammyTicks(bool currentSpSustainState) - { - // Just hit the SP sustain this update - if (!WasSpSustainActive && currentSpSustainState) - { - return 1; - } - - return CurrentTick - LastTick; - } - protected virtual void RebaseProgressValues(uint baseTick) { From 23615ed54dbaf97151e734c176e020c2d130886b Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Sat, 30 Nov 2024 01:11:56 +0000 Subject: [PATCH 20/34] Call UpdateStarPower before hit logic --- YARG.Core/Engine/BaseEngine.cs | 3 +++ YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs | 2 -- YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs | 1 - YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs | 2 -- YARG.Core/Engine/Vocals/Engines/YargVocalsEngine.cs | 2 -- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index 451c9ad6a..61e19b02f 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -437,6 +437,9 @@ private void RunEngineLoop(double time) { ReRunHitLogic = false; UpdateTimeVariables(time); + + UpdateStarPower(); + UpdateHitLogic(time); } while (ReRunHitLogic); } diff --git a/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs b/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs index dd2820c8a..9d2132528 100644 --- a/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs +++ b/YARG.Core/Engine/Drums/Engines/YargDrumsEngine.cs @@ -28,8 +28,6 @@ protected override void MutateStateWithInput(GameInput gameInput) protected override void UpdateHitLogic(double time) { - UpdateStarPower(); - // Update bot (will return if not enabled) UpdateBot(time); diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index 4050fc870..22ea1e55e 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -105,7 +105,6 @@ protected override void MutateStateWithInput(GameInput gameInput) protected override void UpdateHitLogic(double time) { - UpdateStarPower(); UpdateTimers(); bool strumEatenByHopo = false; diff --git a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs index e73b2a1fd..9b4c049c6 100644 --- a/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs +++ b/YARG.Core/Engine/ProKeys/Engines/YargProKeysEngine.cs @@ -50,8 +50,6 @@ protected override void MutateStateWithInput(GameInput gameInput) protected override void UpdateHitLogic(double time) { - UpdateStarPower(); - // Update bot (will return if not enabled) UpdateBot(time); diff --git a/YARG.Core/Engine/Vocals/Engines/YargVocalsEngine.cs b/YARG.Core/Engine/Vocals/Engines/YargVocalsEngine.cs index c37922660..040f5228e 100644 --- a/YARG.Core/Engine/Vocals/Engines/YargVocalsEngine.cs +++ b/YARG.Core/Engine/Vocals/Engines/YargVocalsEngine.cs @@ -68,8 +68,6 @@ protected override void MutateStateWithInput(GameInput gameInput) protected override void UpdateHitLogic(double time) { - UpdateStarPower(); - // Quit early if there are no notes left if (NoteIndex >= Notes.Count) { From 0b2f72521e1e99406aa0cc4a2028132b28e06b1e Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Sat, 30 Nov 2024 01:12:35 +0000 Subject: [PATCH 21/34] Queue update for when star power will reach half during whammy --- YARG.Core/Engine/BaseEngine.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/BaseEngine.cs b/YARG.Core/Engine/BaseEngine.cs index 61e19b02f..1f556559d 100644 --- a/YARG.Core/Engine/BaseEngine.cs +++ b/YARG.Core/Engine/BaseEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using YARG.Core.Chart; @@ -363,6 +363,29 @@ protected virtual void GenerateQueuedUpdates(double nextTime) QueueUpdateTime(StarPowerEndTime, "SP End Time"); } } + else + { + if (StarPowerWhammyTimer.IsActive) + { + var nextTimeTick = SyncTrack.TimeToTick(nextTime); + var tickDelta = nextTimeTick - CurrentTick; + + var ticksAfterWhammy = BaseStats.StarPowerTickAmount + tickDelta; + + if (ticksAfterWhammy >= TicksPerHalfSpBar) + { + var ticksToHalfBar = TicksPerHalfSpBar - BaseStats.StarPowerTickAmount; + var timeOfHalfBar = SyncTrack.TickToTime(CurrentTick + ticksToHalfBar); + + if (IsTimeBetween(timeOfHalfBar, previousTime, nextTime)) + { + YargLogger.LogFormatTrace("Queuing star power half bar time at {0}", + timeOfHalfBar); + QueueUpdateTime(timeOfHalfBar, "Star Power Half Bar"); + } + } + } + } } protected abstract void UpdateTimeVariables(double time); From b0e208d330af359e6301b142de0265caeeceb93f Mon Sep 17 00:00:00 2001 From: RileyTheFox Date: Sun, 1 Dec 2024 18:15:06 +0000 Subject: [PATCH 22/34] remove stupid hack that didnt actually work --- YARG.Core/Chart/Sync/SyncTrack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YARG.Core/Chart/Sync/SyncTrack.cs b/YARG.Core/Chart/Sync/SyncTrack.cs index 521c0bef1..52e1fc3f1 100644 --- a/YARG.Core/Chart/Sync/SyncTrack.cs +++ b/YARG.Core/Chart/Sync/SyncTrack.cs @@ -269,7 +269,7 @@ public static uint TimeRangeToTickDelta(double timeStart, double timeEnd, uint r double timeDelta = timeEnd - timeStart; double beatDelta = timeDelta * currentTempo.BeatsPerMinute / 60.0; - uint tickDelta = (uint)Math.Round(beatDelta * resolution, 8); + uint tickDelta = (uint) (beatDelta * resolution); return tickDelta; } From 0d67aeaa6d1e55a4748a5834942ca6b5c44b8726 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 1 Dec 2024 17:47:59 -0700 Subject: [PATCH 23/34] Adjust various chart-related units for better precision/intent --- YARG.Core/Chart/Events/WaitCountdown.cs | 4 ++-- YARG.Core/Chart/SongChart.AutoGeneration.cs | 4 ++-- YARG.Core/Chart/Sync/TempoChange.cs | 14 +++++++------- YARG.Core/Chart/Sync/TimeSignatureEvent.cs | 2 +- .../MoonscraperChartParser/Events/MoonNote.cs | 6 +++--- .../MoonscraperChartParser/IO/Midi/MidReader.cs | 4 ++-- YARG.Core/MoonscraperChartParser/MoonSong.cs | 4 ++-- YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs | 4 ++-- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/YARG.Core/Chart/Events/WaitCountdown.cs b/YARG.Core/Chart/Events/WaitCountdown.cs index f33feb40a..10d68f511 100644 --- a/YARG.Core/Chart/Events/WaitCountdown.cs +++ b/YARG.Core/Chart/Events/WaitCountdown.cs @@ -5,8 +5,8 @@ namespace YARG.Core.Chart { public class WaitCountdown : ChartEvent { - public const float MIN_SECONDS = 9; - public const float END_COUNTDOWN_SECOND = 1f; + public const double MIN_SECONDS = 9; + public const double END_COUNTDOWN_SECOND = 1; //The time where the countdown should start fading out and overstrums will break combo again public double DeactivateTime => TimeEnd - END_COUNTDOWN_SECOND; diff --git a/YARG.Core/Chart/SongChart.AutoGeneration.cs b/YARG.Core/Chart/SongChart.AutoGeneration.cs index 3fabb7945..56354928d 100644 --- a/YARG.Core/Chart/SongChart.AutoGeneration.cs +++ b/YARG.Core/Chart/SongChart.AutoGeneration.cs @@ -194,8 +194,8 @@ private void ParseForActivationPhrases(InstrumentDifficulty diffChart, } // Limits for placing activation phrases (in seconds) - const float MIN_SPACING_TIME = 2; - const float MAX_SPACING_TIME = 10; + const double MIN_SPACING_TIME = 2; + const double MAX_SPACING_TIME = 10; // Update this time to the latest SP/Solo/Activation phrase encountered for comparison with the above constants // Start parsing after the end of the 1st SP phrase diff --git a/YARG.Core/Chart/Sync/TempoChange.cs b/YARG.Core/Chart/Sync/TempoChange.cs index 9dc1921ed..c83647a84 100644 --- a/YARG.Core/Chart/Sync/TempoChange.cs +++ b/YARG.Core/Chart/Sync/TempoChange.cs @@ -5,14 +5,14 @@ namespace YARG.Core.Chart { public class TempoChange : SyncEvent, IEquatable, ICloneable { - private const float SECONDS_PER_MINUTE = 60f; + private const double SECONDS_PER_MINUTE = 60; - public float BeatsPerMinute { get; } - public float SecondsPerBeat => SECONDS_PER_MINUTE / BeatsPerMinute; + public double BeatsPerMinute { get; } + public double SecondsPerBeat => SECONDS_PER_MINUTE / BeatsPerMinute; public long MilliSecondsPerBeat => BpmToMicroSeconds(BeatsPerMinute) / 1000; public long MicroSecondsPerBeat => BpmToMicroSeconds(BeatsPerMinute); - public TempoChange(float tempo, double time, uint tick) : base(time, tick) + public TempoChange(double tempo, double time, uint tick) : base(time, tick) { BeatsPerMinute = tempo; } @@ -23,7 +23,7 @@ public TempoChange Clone() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long BpmToMicroSeconds(float tempo) + public static long BpmToMicroSeconds(double tempo) { double secondsPerBeat = SECONDS_PER_MINUTE / tempo; double microseconds = secondsPerBeat * 1000 * 1000; @@ -31,11 +31,11 @@ public static long BpmToMicroSeconds(float tempo) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float MicroSecondsToBpm(long usecs) + public static double MicroSecondsToBpm(long usecs) { double secondsPerBeat = usecs / 1000f / 1000f; double tempo = SECONDS_PER_MINUTE / secondsPerBeat; - return (float) tempo; + return tempo; } public static bool operator ==(TempoChange? left, TempoChange? right) diff --git a/YARG.Core/Chart/Sync/TimeSignatureEvent.cs b/YARG.Core/Chart/Sync/TimeSignatureEvent.cs index cba5b6931..daab6462a 100644 --- a/YARG.Core/Chart/Sync/TimeSignatureEvent.cs +++ b/YARG.Core/Chart/Sync/TimeSignatureEvent.cs @@ -4,7 +4,7 @@ namespace YARG.Core.Chart { public partial class TimeSignatureChange : SyncEvent, IEquatable, ICloneable { - public const float QUARTER_NOTE_DENOMINATOR = 4f; + public const double QUARTER_NOTE_DENOMINATOR = 4; public uint Numerator { get; } public uint Denominator { get; } diff --git a/YARG.Core/MoonscraperChartParser/Events/MoonNote.cs b/YARG.Core/MoonscraperChartParser/Events/MoonNote.cs index 1d88fe6b4..56905da87 100644 --- a/YARG.Core/MoonscraperChartParser/Events/MoonNote.cs +++ b/YARG.Core/MoonscraperChartParser/Events/MoonNote.cs @@ -236,7 +236,7 @@ public override int InsertionCompareTo(MoonObject obj) /// /// Ignores the note's forced flag when determining whether it would be a hopo or not /// - public bool IsNaturalHopo(float hopoThreshold) + public bool IsNaturalHopo(uint hopoThreshold) { // Checking state in this order is important return !isChord && @@ -248,7 +248,7 @@ public bool IsNaturalHopo(float hopoThreshold) /// /// Would this note be a hopo or not? (Ignores whether the note's tap flag is set or not.) /// - public bool IsHopo(float hopoThreshold) + public bool IsHopo(uint hopoThreshold) { // F + F || T + T = strum return IsNaturalHopo(hopoThreshold) != forced; @@ -299,7 +299,7 @@ public int GetMaskWithRequiredFlags(Flags flags) /// /// Live calculation of what Note_Type this note would currently be. /// - public MoonNoteType GetGuitarNoteType(float hopoThreshold) + public MoonNoteType GetGuitarNoteType(uint hopoThreshold) { if ((flags & Flags.Tap) != 0) { diff --git a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs index 38ad5768f..c35af9fa2 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs @@ -110,7 +110,7 @@ public static MoonSong ReadMidi(ref ParseSettings settings, MidiFile midi) // Apply settings song.hopoThreshold = settings.HopoThreshold > ParseSettings.SETTING_DEFAULT // +1 for a small bit of leniency - ? (uint)settings.HopoThreshold + 1 + ? (uint)settings.HopoThreshold + 1 : (song.resolution / 3) + 1; if (settings.SustainCutoffThreshold <= ParseSettings.SETTING_DEFAULT) @@ -204,7 +204,7 @@ private static void ReadSync(TempoMap tempoMap, MoonSong song) foreach (var tempo in tempoMap.GetTempoChanges()) { uint tempoTick = (uint) tempo.Time; - song.Add(new TempoChange((float) tempo.Value.BeatsPerMinute, + song.Add(new TempoChange(tempo.Value.BeatsPerMinute, // This is valid since we are guaranteed to have at least one tempo event at all times song.TickToTime(tempoTick, song.syncTrack.Tempos[^1]), tempoTick)); } diff --git a/YARG.Core/MoonscraperChartParser/MoonSong.cs b/YARG.Core/MoonscraperChartParser/MoonSong.cs index 7115b6229..07ffb7a2e 100644 --- a/YARG.Core/MoonscraperChartParser/MoonSong.cs +++ b/YARG.Core/MoonscraperChartParser/MoonSong.cs @@ -171,9 +171,9 @@ public bool Remove(MoonVenue venueEvent) return MoonObjectHelper.Remove(venueEvent, venue); } - public float ResolutionScaleRatio(uint targetResoltion) + public double ResolutionScaleRatio(uint targetResoltion) { - return (float)targetResoltion / resolution; + return (double)targetResoltion / resolution; } public static MoonChart.GameMode InstrumentToChartGameMode(MoonInstrument instrument) diff --git a/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs b/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs index 26cb916ed..0b349fd9b 100644 --- a/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs +++ b/YARG.Core/Song/Entries/Ini/SongEntry.IniBase.cs @@ -60,7 +60,7 @@ static IniSubEntry() public readonly string Background; public readonly string Video; public readonly string Cover; - + public override string Year { get; } public override int YearAsNumber { get; } public override bool LoopVideo { get; } @@ -195,7 +195,7 @@ protected static (ScanResult Result, AvailableParts Parts, LoaderSettings Settin // With a 192 resolution, .chart has a HOPO threshold of 65 ticks, not 64, // so we need to scale this factor to different resolutions (480 res = 162.5 threshold). // Why?... idk, but I hate it. - const float DEFAULT_RESOLUTION = 192; + const double DEFAULT_RESOLUTION = 192; settings.HopoThreshold += (long) (results.resolution / DEFAULT_RESOLUTION); } } From 1ce91c8a07b9728954227cea00bed09a97bd39a5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 2 Dec 2024 02:25:21 -0700 Subject: [PATCH 24/34] Address tick <-> time conversion precision issues --- YARG.Core/Chart/Sync/SyncTrack.cs | 42 +++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/YARG.Core/Chart/Sync/SyncTrack.cs b/YARG.Core/Chart/Sync/SyncTrack.cs index 52e1fc3f1..37a03995e 100644 --- a/YARG.Core/Chart/Sync/SyncTrack.cs +++ b/YARG.Core/Chart/Sync/SyncTrack.cs @@ -242,16 +242,24 @@ public static double TickRangeToTimeDelta(uint tickStart, uint tickEnd, uint res TempoChange currentTempo) { if (tickStart < currentTempo.Tick) + { throw new ArgumentOutOfRangeException(nameof(tickStart), tickStart, $"The given start tick must occur during the given tempo (starting at {currentTempo.Tick})!"); + } if (tickEnd < tickStart) + { throw new ArgumentOutOfRangeException(nameof(tickEnd), tickEnd, $"The given end tick must occur after the starting tick ({tickStart})!"); + } + + double tickDelta = tickEnd - tickStart; - uint tickDelta = tickEnd - tickStart; - double beatDelta = tickDelta / (double)resolution; - double timeDelta = beatDelta * 60.0 / currentTempo.BeatsPerMinute; + // The active code below is a slightly more precise version of the commented code, + // it seems to incur fewer rounding errors and should improve consistency. + // double beatDelta = tickDelta / resolution; + // double timeDelta = beatDelta * currentTempo.SecondsPerBeat; + double timeDelta = (tickDelta * 60.0) / (resolution * currentTempo.BeatsPerMinute); return timeDelta; } @@ -260,18 +268,38 @@ public static uint TimeRangeToTickDelta(double timeStart, double timeEnd, uint r TempoChange currentTempo) { if (timeStart < currentTempo.Time) + { throw new ArgumentOutOfRangeException(nameof(timeStart), timeStart, - $"The given start time must occur during the given tempo (starting at {currentTempo.Tick})!"); + $"The given start time must occur during the given tempo (starting at {currentTempo.Time})!"); + } if (timeEnd < timeStart) + { throw new ArgumentOutOfRangeException(nameof(timeEnd), timeEnd, $"The given end time must occur after the starting time ({timeStart})!"); + } double timeDelta = timeEnd - timeStart; - double beatDelta = timeDelta * currentTempo.BeatsPerMinute / 60.0; - uint tickDelta = (uint) (beatDelta * resolution); - return tickDelta; + // The active code below is a slightly more precise version of the commented code, + // it seems to incur fewer rounding errors and should improve consistency somewhat. + // double beatDelta = timeDelta / currentTempo.SecondsPerBeat; + // double tickDelta = beatDelta * resolution; + double tickDelta = (timeDelta * resolution * currentTempo.BeatsPerMinute) / 60.0; + + // Despite the precision improvements above, there are still some floating-point imprecisions, + // making time <-> tick conversions not accurately round-trippable. Thus, we need to round to + // prevent truncation from resulting in the wrong tick. + // + // A more conservative approach is taken here, where rounding is only done if the result is + // within 0.1 of the next whole value. + double deltaDecimals = (tickDelta + 1) - tickDelta; + if (Math.Abs(deltaDecimals) >= 0.9) + { + tickDelta += 0.1; + } + + return (uint) tickDelta; } public double GetStartTime() From 45d62370f7fafad770ec41ae3b923dc4abbffad5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 2 Dec 2024 02:26:13 -0700 Subject: [PATCH 25/34] Change .mid reader warning logs to debug logs These particular ones can get quite spammy, and are only really helpful for developers --- .../MoonscraperChartParser/IO/Midi/MidReader.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs index c35af9fa2..2a52115ca 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs @@ -329,7 +329,7 @@ private static void ReadVenueEvents(TrackChunk track, MoonSong song) { // Check for duplicates if (TryFindMatchingNote(unpairedNoteQueue, note, out _, out _, out _)) - YargLogger.LogFormatWarning("Found duplicate note on at tick {0}!", absoluteTime); + YargLogger.LogFormatDebug("Found duplicate note on at tick {0}!", absoluteTime); else unpairedNoteQueue.Add((note, absoluteTime)); } @@ -338,7 +338,7 @@ private static void ReadVenueEvents(TrackChunk track, MoonSong song) // Find starting note if (!TryFindMatchingNote(unpairedNoteQueue, note, out var noteStart, out long startTick, out int startIndex)) { - YargLogger.LogFormatWarning("Found note off with no corresponding note on at tick {0}!", absoluteTime); + YargLogger.LogFormatDebug("Found note off with no corresponding note on at tick {0}!", absoluteTime); return; } unpairedNoteQueue.RemoveAt(startIndex); @@ -495,7 +495,7 @@ private static void ProcessNoteEvent(ref EventProcessParams processParams, NoteE { // Check for duplicates if (TryFindMatchingNote(unpairedNotes, note, out _, out _, out _)) - YargLogger.LogFormatWarning("Found duplicate note on at tick {0}!", absoluteTick); + YargLogger.LogFormatDebug("Found duplicate note on at tick {0}!", absoluteTick); else unpairedNotes.Add((note, absoluteTick)); } @@ -503,7 +503,7 @@ private static void ProcessNoteEvent(ref EventProcessParams processParams, NoteE { if (!TryFindMatchingNote(unpairedNotes, note, out var noteStart, out long startTick, out int startIndex)) { - YargLogger.LogFormatWarning("Found note off with no corresponding note on at tick {0}!", absoluteTick); + YargLogger.LogFormatDebug("Found note off with no corresponding note on at tick {0}!", absoluteTick); return; } unpairedNotes.RemoveAt(startIndex); @@ -550,14 +550,14 @@ private static void ProcessSysExEvent(ref EventProcessParams processParams, SysE if (!PhaseShiftSysEx.TryParse(sysex, out var psEvent)) { // SysEx event is not a Phase Shift SysEx event - YargLogger.LogFormatWarning("Encountered unknown SysEx event at tick {0}: {1}", + YargLogger.LogFormatDebug("Encountered unknown SysEx event at tick {0}: {1}", absoluteTick, new HexBytesFormat(sysex.Data)); return; } if (psEvent.type != PhaseShiftSysEx.Type.Phrase) { - YargLogger.LogFormatWarning("Encountered unknown Phase Shift SysEx event type {0} at tick {1}!", + YargLogger.LogFormatDebug("Encountered unknown Phase Shift SysEx event type {0} at tick {1}!", psEvent.type, absoluteTick); return; } @@ -566,7 +566,7 @@ private static void ProcessSysExEvent(ref EventProcessParams processParams, SysE { // Check for duplicates if (TryFindMatchingSysEx(unpairedSysex, psEvent, out _, out _, out _)) - YargLogger.LogFormatWarning("Found duplicate SysEx start event at tick {0}!", absoluteTick); + YargLogger.LogFormatDebug("Found duplicate SysEx start event at tick {0}!", absoluteTick); else unpairedSysex.Add((psEvent, absoluteTick)); } @@ -574,7 +574,7 @@ private static void ProcessSysExEvent(ref EventProcessParams processParams, SysE { if (!TryFindMatchingSysEx(unpairedSysex, psEvent, out var sysexStart, out long startTick, out int startIndex)) { - YargLogger.LogFormatWarning("Found PS SysEx end with no corresponding start at tick {0}!", absoluteTick); + YargLogger.LogFormatDebug("Found PS SysEx end with no corresponding start at tick {0}!", absoluteTick); return; } unpairedSysex.RemoveAt(startIndex); From a01e2857c37c6d148f725b1a270765b35c33fe8b Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 2 Dec 2024 13:25:39 -0700 Subject: [PATCH 26/34] Make .chart tempo loading use double for division Also replace usage of chart event tracker with direct access of tempo list, since we already make that assumption --- .../MoonscraperChartParser/IO/Chart/ChartReader.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/YARG.Core/MoonscraperChartParser/IO/Chart/ChartReader.cs b/YARG.Core/MoonscraperChartParser/IO/Chart/ChartReader.cs index 9d12ffc38..f27ce7c31 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Chart/ChartReader.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Chart/ChartReader.cs @@ -313,9 +313,6 @@ private static MoonSong SubmitDataSong(AsciiTrimSplitter sectionLines) private static void SubmitDataSync(MoonSong song, AsciiTrimSplitter sectionLines) { uint prevTick = 0; - - // This is valid since we are guaranteed to have at least one tempo event at all times - var tempoTracker = new ChartEventTickTracker(song.syncTrack.Tempos); foreach (var _line in sectionLines) { var line = _line.Trim(); @@ -332,7 +329,8 @@ private static void SubmitDataSync(MoonSong song, AsciiTrimSplitter sectionLines if (prevTick > tick) throw new Exception("Tick value not in ascending order"); prevTick = tick; - tempoTracker.Update(tick); + // This is valid since we are guaranteed to have at least one tempo event at all times + var currentTempo = song.syncTrack.Tempos[^1]; // Get event type var typeCode = remaining.GetNextWord(out remaining); @@ -342,7 +340,7 @@ private static void SubmitDataSync(MoonSong song, AsciiTrimSplitter sectionLines var tempoText = remaining.GetNextWord(out remaining); uint tempo = (uint) FastInt32Parse(tempoText); - song.Add(new TempoChange(tempo / 1000f, song.TickToTime(tick, tempoTracker.Current!), tick)); + song.Add(new TempoChange(tempo / 1000.0, song.TickToTime(tick, currentTempo), tick)); } else if (typeCode.Equals("TS", StringComparison.Ordinal)) { @@ -354,7 +352,7 @@ private static void SubmitDataSync(MoonSong song, AsciiTrimSplitter sectionLines var denominatorText = remaining.GetNextWord(out remaining); uint denominator = denominatorText.IsEmpty ? 2 : (uint) FastInt32Parse(denominatorText); song.Add(new TimeSignatureChange(numerator, (uint) Math.Pow(2, denominator), - song.TickToTime(tick, tempoTracker.Current!), tick)); + song.TickToTime(tick, currentTempo), tick)); } else if (typeCode.Equals("A", StringComparison.Ordinal)) { From 5aae3aa5cba23b31ba43e2cd9afb37969c9f2959 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 2 Dec 2024 13:29:35 -0700 Subject: [PATCH 27/34] Refactor .mid reader to parse tempo track manually --- .../IO/Midi/MidReader.cs | 81 ++++++++++++------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs index 2a52115ca..393b14cfa 100644 --- a/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs +++ b/YARG.Core/MoonscraperChartParser/IO/Midi/MidReader.cs @@ -1,13 +1,10 @@ -// Copyright (c) 2016-2020 Alexander Ong +// Copyright (c) 2016-2020 Alexander Ong // See LICENSE in project root for license information. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using Melanchall.DryWetMidi.Core; -using Melanchall.DryWetMidi.Interaction; -using YARG.Core; using YARG.Core.Chart; using YARG.Core.Extensions; using YARG.Core.Logging; @@ -100,12 +97,16 @@ public static MoonSong ReadMidi(ref ParseSettings settings, Stream stream) public static MoonSong ReadMidi(ref ParseSettings settings, MidiFile midi) { if (midi.Chunks == null || midi.Chunks.Count < 1) - throw new InvalidOperationException("MIDI file has no tracks, unable to parse."); + { + throw new InvalidDataException("MIDI file has no tracks, unable to parse."); + } if (midi.TimeDivision is not TicksPerQuarterNoteTimeDivision ticks) - throw new InvalidOperationException("MIDI file has no beat resolution set!"); + { + throw new InvalidDataException("MIDI file has no beat resolution set!"); + } - var song = new MoonSong((uint)ticks.TicksPerQuarterNote); + var song = new MoonSong((uint) ticks.TicksPerQuarterNote); // Apply settings song.hopoThreshold = settings.HopoThreshold > ParseSettings.SETTING_DEFAULT @@ -123,14 +124,26 @@ public static MoonSong ReadMidi(ref ParseSettings settings, MidiFile midi) settings.SustainCutoffThreshold = 1; } - // Read all bpm data in first. This will also allow song.TimeToTick to function properly. - ReadSync(midi.GetTempoMap(), song); + // The sync track is the very first track in the file + var syncChunk = midi.Chunks[0]; + if (syncChunk is not TrackChunk syncTrack) + { + throw new InvalidDataException($"MIDI file has no sync track! Found chunk with ID {syncChunk.ChunkId} instead"); + } + ReadSync(syncTrack, song); - foreach (var track in midi.GetTrackChunks()) + for (int i = 1; i < midi.Chunks.Count; i++) { - if (track == null || track.Events.Count < 1) + var chunk = midi.Chunks[i]; + if (chunk is not TrackChunk track) { - YargLogger.LogTrace("Encountered an empty MIDI track!"); + YargLogger.LogFormatDebug("Found non-track chunk {0} in MIDI file!", chunk.ChunkId); + continue; + } + + if (track.Events.Count < 1) + { + YargLogger.LogFormatDebug("Track {0} in MIDI file is empty!", i); continue; } @@ -197,25 +210,34 @@ public static MoonSong ReadMidi(ref ParseSettings settings, MidiFile midi) return song; } - private static void ReadSync(TempoMap tempoMap, MoonSong song) + private static void ReadSync(TrackChunk track, MoonSong song) { - YargLogger.LogTrace("Reading sync track"); + if (track.Events.Count < 1) + return; - foreach (var tempo in tempoMap.GetTempoChanges()) + YargLogger.LogTrace("Reading sync track"); + long absoluteTick = track.Events[0].DeltaTime; + for (int i = 0; i < track.Events.Count; i++) { - uint tempoTick = (uint) tempo.Time; - song.Add(new TempoChange(tempo.Value.BeatsPerMinute, - // This is valid since we are guaranteed to have at least one tempo event at all times - song.TickToTime(tempoTick, song.syncTrack.Tempos[^1]), tempoTick)); - } + var trackEvent = track.Events[i]; + absoluteTick += trackEvent.DeltaTime; - var tempoTracker = new ChartEventTickTracker(song.syncTrack.Tempos); - foreach (var timesig in tempoMap.GetTimeSignatureChanges()) - { - uint tsTick = (uint) timesig.Time; - tempoTracker.Update(tsTick); - song.Add(new TimeSignatureChange((uint) timesig.Value.Numerator, (uint) timesig.Value.Denominator, - song.TickToTime(tsTick, tempoTracker.Current!), tsTick)); + uint tick = (uint) absoluteTick; + + // This is valid since we are guaranteed to have at least one tempo event at all times + var currentTempo = song.syncTrack.Tempos[^1]; + + if (trackEvent is SetTempoEvent tempo) + { + double bpm = TempoChange.MicroSecondsToBpm(tempo.MicrosecondsPerQuarterNote); + song.Add(new TempoChange(bpm, + song.TickToTime(tick, currentTempo), tick)); + } + else if (trackEvent is TimeSignatureEvent timesig) + { + song.Add(new TimeSignatureChange(timesig.Numerator, timesig.Denominator, + song.TickToTime(tick, currentTempo), tick)); + } } } @@ -226,6 +248,7 @@ private static void ReadSongBeats(TrackChunk track, MoonSong song) YargLogger.LogTrace("Reading beat track"); long absoluteTime = track.Events[0].DeltaTime; + // First event is the track name event, which gets skipped for (int i = 1; i < track.Events.Count; i++) { var trackEvent = track.Events[i]; @@ -258,6 +281,7 @@ private static void ReadSongGlobalEvents(TrackChunk track, MoonSong song) YargLogger.LogTrace("Reading global events"); long absoluteTime = track.Events[0].DeltaTime; + // First event is the track name event, which gets skipped for (int i = 1; i < track.Events.Count; i++) { var trackEvent = track.Events[i]; @@ -288,6 +312,7 @@ private static void ReadTextEventsIntoGlobalEventsAsLyrics(TrackChunk track, Moo YargLogger.LogTrace("Reading global lyrics"); long absoluteTime = track.Events[0].DeltaTime; + // First event is the track name event, which gets skipped for (int i = 1; i < track.Events.Count; i++) { var trackEvent = track.Events[i]; @@ -318,6 +343,7 @@ private static void ReadVenueEvents(TrackChunk track, MoonSong song) var unpairedNoteQueue = new NoteEventQueue(); long absoluteTime = track.Events[0].DeltaTime; + // First event is the track name event, which gets skipped for (int i = 1; i < track.Events.Count; i++) { var trackEvent = track.Events[i]; @@ -424,6 +450,7 @@ private static void ReadNotes(ref ParseSettings settings, TrackChunk track, Moon // Load all the notes long absoluteTick = track.Events[0].DeltaTime; + // First event is the track name event, which gets skipped for (int i = 1; i < track.Events.Count; i++) { var trackEvent = track.Events[i]; From 1c725febc6cf3e56b7a2639e1f333eedbd34391a Mon Sep 17 00:00:00 2001 From: Nathan Mills Date: Wed, 27 Nov 2024 01:32:13 -0500 Subject: [PATCH 28/34] Engine support for treating solo frets separately from normal frets --- .../Guitar/Engines/YargFiveFretEngine.cs | 3 ++- YARG.Core/Engine/Guitar/GuitarEngine.cs | 18 ++++++++++++++++++ YARG.Core/Input/InputActions.cs | 12 ++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index 22ea1e55e..a71edc6b1 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -247,7 +247,8 @@ protected override void CheckForNoteHit() // If a note is a tap then it can be hit only if it is the closest note, unless // the combo is 0 then it can be hit regardless of the distance (note skipping) - bool tapCondition = note.IsTap && (isFirstNoteInWindow || EngineStats.Combo == 0); + // Need a solo condition that combines IsSoloActive and that the input was a solo button + bool tapCondition = (note.IsTap || (IsSoloActive && ButtonIsSolo)) && (isFirstNoteInWindow || EngineStats.Combo == 0); bool frontEndIsExpired = note.Time > FrontEndExpireTime; bool canUseInfFrontEnd = diff --git a/YARG.Core/Engine/Guitar/GuitarEngine.cs b/YARG.Core/Engine/Guitar/GuitarEngine.cs index 56d9d34da..b64b51123 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngine.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngine.cs @@ -18,6 +18,8 @@ public abstract class GuitarEngine : BaseEngine= 10) { + fret -= 10; + ButtonIsSolo = true; + } else { + ButtonIsSolo = false; + } ButtonMask = (byte) (active ? ButtonMask | (1 << fret) : ButtonMask & ~(1 << fret)); } public bool IsFretHeld(GuitarAction fret) { + // FIXME: This is a terrible hack + if((int) fret >= 10) { + fret -= 10; + } return (ButtonMask & (1 << (int) fret)) != 0; } @@ -375,6 +388,11 @@ GuitarAction.RedFret or GuitarAction.YellowFret or GuitarAction.BlueFret or GuitarAction.OrangeFret or + GuitarAction.SoloGreenFret or + GuitarAction.SoloRedFret or + GuitarAction.SoloYellowFret or + GuitarAction.SoloBlueFret or + GuitarAction.SoloOrangeFret or GuitarAction.White3Fret => true, _ => false, }; diff --git a/YARG.Core/Input/InputActions.cs b/YARG.Core/Input/InputActions.cs index 44c9f5ac8..eaadbbf53 100644 --- a/YARG.Core/Input/InputActions.cs +++ b/YARG.Core/Input/InputActions.cs @@ -62,6 +62,11 @@ public enum GuitarAction : byte /// The Star Power action, reported as a button. StarPower = 9, + Fret11 = 10, + Fret12 = 11, + Fret13 = 12, + Fret14 = 13, + Fret15 = 14, /// (5-fret) Green fret button. /// Alias of . GreenFret = Fret1, @@ -96,6 +101,13 @@ public enum GuitarAction : byte /// (6-fret) White 3 fret button. /// Alias of . White3Fret = Fret6, + + // Aliases for solo frets + SoloGreenFret = Fret11, + SoloRedFret = Fret12, + SoloYellowFret = Fret13, + SoloBlueFret = Fret14, + SoloOrangeFret = Fret15, } /// From dec820c5f86b15fe60331d7c6cc97a81b5a722df Mon Sep 17 00:00:00 2001 From: Nathan Mills Date: Wed, 27 Nov 2024 21:21:58 -0500 Subject: [PATCH 29/34] Make solo taps an engine parameter --- YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs | 3 ++- YARG.Core/Engine/Guitar/GuitarEngineParameters.cs | 9 ++++++++- YARG.Core/Game/Presets/EnginePreset.Instruments.cs | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index a71edc6b1..25d4967a3 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -248,7 +248,8 @@ protected override void CheckForNoteHit() // If a note is a tap then it can be hit only if it is the closest note, unless // the combo is 0 then it can be hit regardless of the distance (note skipping) // Need a solo condition that combines IsSoloActive and that the input was a solo button - bool tapCondition = (note.IsTap || (IsSoloActive && ButtonIsSolo)) && (isFirstNoteInWindow || EngineStats.Combo == 0); + bool tapCondition = (note.IsTap || (IsSoloActive && ButtonIsSolo && EngineParameters.SoloTaps)) && + (isFirstNoteInWindow || EngineStats.Combo == 0); bool frontEndIsExpired = note.Time > FrontEndExpireTime; bool canUseInfFrontEnd = diff --git a/YARG.Core/Engine/Guitar/GuitarEngineParameters.cs b/YARG.Core/Engine/Guitar/GuitarEngineParameters.cs index a3c02f6de..068af999c 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngineParameters.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngineParameters.cs @@ -10,10 +10,11 @@ public class GuitarEngineParameters : BaseEngineParameters public readonly double StrumLeniencySmall; public readonly bool InfiniteFrontEnd; public readonly bool AntiGhosting; + public readonly bool SoloTaps; public GuitarEngineParameters(HitWindowSettings hitWindow, int maxMultiplier, double spWhammyBuffer, double sustainDropLeniency, float[] starMultiplierThresholds, double hopoLeniency, double strumLeniency, - double strumLeniencySmall, bool infiniteFrontEnd, bool antiGhosting) + double strumLeniencySmall, bool infiniteFrontEnd, bool antiGhosting, bool soloTaps) : base(hitWindow, maxMultiplier, spWhammyBuffer, sustainDropLeniency, starMultiplierThresholds) { HopoLeniency = hopoLeniency; @@ -23,6 +24,8 @@ public GuitarEngineParameters(HitWindowSettings hitWindow, int maxMultiplier, do InfiniteFrontEnd = infiniteFrontEnd; AntiGhosting = antiGhosting; + + SoloTaps = soloTaps; } public GuitarEngineParameters(UnmanagedMemoryStream stream, int version) @@ -35,6 +38,7 @@ public GuitarEngineParameters(UnmanagedMemoryStream stream, int version) InfiniteFrontEnd = stream.ReadBoolean(); AntiGhosting = stream.ReadBoolean(); + SoloTaps = stream.ReadBoolean(); } public override void Serialize(BinaryWriter writer) @@ -48,6 +52,8 @@ public override void Serialize(BinaryWriter writer) writer.Write(InfiniteFrontEnd); writer.Write(AntiGhosting); + + writer.Write(SoloTaps); } public override string ToString() @@ -56,6 +62,7 @@ public override string ToString() $"{base.ToString()}\n" + $"Infinite front-end: {InfiniteFrontEnd}\n" + $"Anti-ghosting: {AntiGhosting}\n" + + $"Solo taps: {SoloTaps}\n" + $"Hopo leniency: {HopoLeniency}\n" + $"Strum leniency: {StrumLeniency}\n" + $"Strum leniency (small): {StrumLeniencySmall}\n" + diff --git a/YARG.Core/Game/Presets/EnginePreset.Instruments.cs b/YARG.Core/Game/Presets/EnginePreset.Instruments.cs index 15ffed6f0..2237917b6 100644 --- a/YARG.Core/Game/Presets/EnginePreset.Instruments.cs +++ b/YARG.Core/Game/Presets/EnginePreset.Instruments.cs @@ -79,6 +79,9 @@ public class FiveFretGuitarPreset [SettingType(SettingType.Toggle)] public bool InfiniteFrontEnd = false; + [SettingType(SettingType.Toggle)] + public bool SoloTaps = false; + [SettingType(SettingType.MillisecondInput)] [SettingRange(min: 0f)] public double HopoLeniency = 0.08; @@ -130,7 +133,8 @@ public GuitarEngineParameters Create(float[] starMultiplierThresholds, bool isBa StrumLeniency, StrumLeniencySmall, InfiniteFrontEnd, - AntiGhosting); + AntiGhosting, + SoloTaps); } } From 81facbe3f7605db8d07fa62bc9270e49bd5a0fc9 Mon Sep 17 00:00:00 2001 From: Nathan Mills Date: Wed, 27 Nov 2024 21:50:54 -0500 Subject: [PATCH 30/34] Add SoloTaps to FiveFretGuitarPreset::Copy() --- YARG.Core/Game/Presets/EnginePreset.Instruments.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/YARG.Core/Game/Presets/EnginePreset.Instruments.cs b/YARG.Core/Game/Presets/EnginePreset.Instruments.cs index 2237917b6..6a9ef94b9 100644 --- a/YARG.Core/Game/Presets/EnginePreset.Instruments.cs +++ b/YARG.Core/Game/Presets/EnginePreset.Instruments.cs @@ -117,6 +117,7 @@ public FiveFretGuitarPreset Copy() StrumLeniency = StrumLeniency, StrumLeniencySmall = StrumLeniencySmall, HitWindow = HitWindow.Copy(), + SoloTaps = SoloTaps, }; } From 79f596cc45b86ac7001b09c25b4c8474575653b7 Mon Sep 17 00:00:00 2001 From: Nathan Mills Date: Thu, 28 Nov 2024 15:59:28 -0500 Subject: [PATCH 31/34] Replace IsSoloButton flag with StandardButtonCount int and reverse solo tap logic to not trigger when any standard buttons are fretted. --- .../Guitar/Engines/YargFiveFretEngine.cs | 8 +++++--- YARG.Core/Engine/Guitar/GuitarEngine.cs | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index 25d4967a3..ae1b0d172 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -240,6 +240,10 @@ protected override void CheckForNoteHit() continue; } + // Defines whether solo tapping is allowed + // Only if SoloTaps engine parameter is set, solo is active, and no non-solo buttons are pressed + bool SoloTapAllowed = EngineParameters.SoloTaps && IsSoloActive && !(StandardButtonCount > 0); + // Handles hitting a hopo notes // If first note is a hopo then it can be hit without combo (for practice mode) bool hopoCondition = note.IsHopo && isFirstNoteInWindow && @@ -247,9 +251,7 @@ protected override void CheckForNoteHit() // If a note is a tap then it can be hit only if it is the closest note, unless // the combo is 0 then it can be hit regardless of the distance (note skipping) - // Need a solo condition that combines IsSoloActive and that the input was a solo button - bool tapCondition = (note.IsTap || (IsSoloActive && ButtonIsSolo && EngineParameters.SoloTaps)) && - (isFirstNoteInWindow || EngineStats.Combo == 0); + bool tapCondition = (note.IsTap || SoloTapAllowed) && (isFirstNoteInWindow || EngineStats.Combo == 0); bool frontEndIsExpired = note.Time > FrontEndExpireTime; bool canUseInfFrontEnd = diff --git a/YARG.Core/Engine/Guitar/GuitarEngine.cs b/YARG.Core/Engine/Guitar/GuitarEngine.cs index b64b51123..d39aef29c 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngine.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngine.cs @@ -18,7 +18,10 @@ public abstract class GuitarEngine : BaseEngine= 10) { fret -= 10; - ButtonIsSolo = true; } else { - ButtonIsSolo = false; + if (active) + { + // Add one to count of standard buttons pressed, avoiding overflow + StandardButtonCount = (StandardButtonCount < int.MaxValue) ? StandardButtonCount + 1 : StandardButtonCount; + } + else + { + // Subtract one from count, avoiding underflow + StandardButtonCount = (StandardButtonCount > 0) ? StandardButtonCount - 1 : 0; + } } ButtonMask = (byte) (active ? ButtonMask | (1 << fret) : ButtonMask & ~(1 << fret)); } @@ -373,7 +384,8 @@ protected void ToggleFret(int fret, bool active) public bool IsFretHeld(GuitarAction fret) { // FIXME: This is a terrible hack - if((int) fret >= 10) { + if((int) fret >= 10) + { fret -= 10; } return (ButtonMask & (1 << (int) fret)) != 0; From fe4a19b60e3bf1897e507bd4aa3445fe4d1e022a Mon Sep 17 00:00:00 2001 From: Nathan Mills Date: Sun, 8 Dec 2024 15:11:30 -0500 Subject: [PATCH 32/34] Fix issue with solo tap not being allowed on first note of solo. It turns out that IsSoloActive doesn't get set until after the first note of the solo is hit or missed, so SoloTapAllowed now allows a note to be tapped when either IsSoloActive is true or if the note has the IsSoloStart flag. --- YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index ae1b0d172..524aae700 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -242,7 +242,8 @@ protected override void CheckForNoteHit() // Defines whether solo tapping is allowed // Only if SoloTaps engine parameter is set, solo is active, and no non-solo buttons are pressed - bool SoloTapAllowed = EngineParameters.SoloTaps && IsSoloActive && !(StandardButtonCount > 0); + // Also allow tap if the note is a solo start note, since IsSoloActive isn't set until after this point + bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !(StandardButtonCount > 0); // Handles hitting a hopo notes // If first note is a hopo then it can be hit without combo (for practice mode) From 40758c35a4a73c581fc28cf1aba8bd0866944449 Mon Sep 17 00:00:00 2001 From: wyrdough Date: Sun, 8 Dec 2024 21:45:44 -0500 Subject: [PATCH 33/34] Add SoloTapAllowed clause to hopoleniency check in HitNote() This fixes an issue where it's way too easy to end up overstrumming when attempting to strum notes fretted with the solo buttons during solos. --- YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index 524aae700..c966d00e4 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -423,7 +423,10 @@ static bool IsNoteHittable(GuitarNote note, byte buttonsMasked) protected override void HitNote(GuitarNote note) { - if (note.IsHopo || note.IsTap) + // Hopo leniency needs to activate on solo taps also + bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !(StandardButtonCount > 0); + + if (note.IsHopo || note.IsTap || SoloTapAllowed) { HasTapped = false; StartTimer(ref HopoLeniencyTimer, CurrentTime); From f5f890365ccfc7bc21bb391716c50486665c9f4f Mon Sep 17 00:00:00 2001 From: wyrdough Date: Sun, 19 Jan 2025 14:02:45 -0500 Subject: [PATCH 34/34] Rework solo button detection --- .../Guitar/Engines/YargFiveFretEngine.cs | 47 ++++++++--------- YARG.Core/Engine/Guitar/GuitarEngine.cs | 51 +++++++------------ 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs index c966d00e4..76cd8e5b6 100644 --- a/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs +++ b/YARG.Core/Engine/Guitar/Engines/YargFiveFretEngine.cs @@ -27,12 +27,12 @@ protected override void UpdateBot(double time) return; } - LastButtonMask = ButtonMask; - ButtonMask = (byte) note.NoteMask; + LastButtonMask = EffectiveButtonMask; + EffectiveButtonMask = (byte) note.NoteMask; - YargLogger.LogFormatTrace("[Bot] Set button mask to: {0}", ButtonMask); + YargLogger.LogFormatTrace("[Bot] Set button mask to: {0}", EffectiveButtonMask); - HasTapped = ButtonMask != LastButtonMask; + HasTapped = EffectiveButtonMask != LastButtonMask; IsFretPress = true; HasStrummed = false; StrumLeniencyTimer.Start(time); @@ -48,15 +48,15 @@ protected override void UpdateBot(double time) if (sustainNote.IsDisjoint) { - ButtonMask |= (byte) sustainNote.DisjointMask; + EffectiveButtonMask |= (byte) sustainNote.DisjointMask; - YargLogger.LogFormatTrace("[Bot] Added Disjoint Sustain Mask {0} to button mask. {1}", sustainNote.DisjointMask, ButtonMask); + YargLogger.LogFormatTrace("[Bot] Added Disjoint Sustain Mask {0} to button mask. {1}", sustainNote.DisjointMask, EffectiveButtonMask); } else { - ButtonMask |= (byte) sustainNote.NoteMask; + EffectiveButtonMask |= (byte) sustainNote.NoteMask; - YargLogger.LogFormatTrace("[Bot] Added Sustain Mask {0} to button mask. {1}", sustainNote.NoteMask, ButtonMask); + YargLogger.LogFormatTrace("[Bot] Added Sustain Mask {0} to button mask. {1}", sustainNote.NoteMask, EffectiveButtonMask); } } } @@ -81,26 +81,26 @@ protected override void MutateStateWithInput(GameInput gameInput) } else if (IsFretInput(gameInput)) { - LastButtonMask = ButtonMask; + LastButtonMask = EffectiveButtonMask; HasFretted = true; IsFretPress = gameInput.Button; ToggleFret(gameInput.Action, gameInput.Button); // No other frets are held, enable the "open fret" - if ((ButtonMask & ~OPEN_MASK) == 0) + if ((EffectiveButtonMask & ~OPEN_MASK) == 0) { - ButtonMask |= OPEN_MASK; + EffectiveButtonMask |= OPEN_MASK; } else { // Some frets are held, disable the "open fret" - ButtonMask &= unchecked((byte) ~OPEN_MASK); + EffectiveButtonMask &= unchecked((byte) ~OPEN_MASK); } } YargLogger.LogFormatTrace("Mutated input state: Button Mask: {0}, HasFretted: {1}, HasStrummed: {2}", - ButtonMask, HasFretted, HasStrummed); + EffectiveButtonMask, HasFretted, HasStrummed); } protected override void UpdateHitLogic(double time) @@ -222,7 +222,7 @@ protected override void CheckForNoteHit() if (!CanNoteBeHit(note)) { YargLogger.LogFormatTrace("Cant hit note (Index: {0}, Mask {1}) at {2}. Buttons: {3}", i, - note.NoteMask, CurrentTime, ButtonMask); + note.NoteMask, CurrentTime, EffectiveButtonMask); // This does nothing special, it's just logging strum leniency if (isFirstNoteInWindow && HasStrummed && StrumLeniencyTimer.IsActive) { @@ -243,7 +243,7 @@ protected override void CheckForNoteHit() // Defines whether solo tapping is allowed // Only if SoloTaps engine parameter is set, solo is active, and no non-solo buttons are pressed // Also allow tap if the note is a solo start note, since IsSoloActive isn't set until after this point - bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !(StandardButtonCount > 0); + bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !StandardButtonHeld; // Handles hitting a hopo notes // If first note is a hopo then it can be hit without combo (for practice mode) @@ -290,7 +290,7 @@ protected override void CheckForNoteHit() protected override bool CanNoteBeHit(GuitarNote note) { - byte buttonsMasked = ButtonMask; + ushort buttonsMasked = EffectiveButtonMask; if (ActiveSustains.Count > 0) { foreach (var sustain in ActiveSustains) @@ -315,7 +315,7 @@ protected override bool CanNoteBeHit(GuitarNote note) // If the resulting masked buttons are 0, we need to apply the Open Mask so open notes can be hit // Need to make a copy of the button mask to prevent modifying the original - byte buttonMaskCopy = ButtonMask; + ushort buttonMaskCopy = EffectiveButtonMask; if (buttonsMasked == 0) { buttonsMasked |= OPEN_MASK; @@ -330,9 +330,9 @@ protected override bool CanNoteBeHit(GuitarNote note) } // If masked/extended sustain logic didn't work, try original ButtonMask - return IsNoteHittable(note, ButtonMask); + return IsNoteHittable(note, EffectiveButtonMask); - static bool IsNoteHittable(GuitarNote note, byte buttonsMasked) + static bool IsNoteHittable(GuitarNote note, ushort buttonsMasked) { // Only used for sustain logic bool useDisjointSustainMask = note is { IsDisjoint: true, WasHit: true }; @@ -423,8 +423,9 @@ static bool IsNoteHittable(GuitarNote note, byte buttonsMasked) protected override void HitNote(GuitarNote note) { - // Hopo leniency needs to activate on solo taps also - bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !(StandardButtonCount > 0); + // Defines whether solo tapping is allowed + // Only if SoloTaps engine parameter is set, solo is active, and no non-solo buttons are pressed + bool SoloTapAllowed = EngineParameters.SoloTaps && (IsSoloActive || note.IsSoloStart) && !StandardButtonHeld; if (note.IsHopo || note.IsTap || SoloTapAllowed) { @@ -500,10 +501,10 @@ protected bool CheckForGhostInput(GuitarNote note) } // Input is a hammer-on if the highest fret held is higher than the highest fret of the previous mask - bool isHammerOn = GetMostSignificantBit(ButtonMask) > GetMostSignificantBit(LastButtonMask); + bool isHammerOn = GetMostSignificantBit(EffectiveButtonMask) > GetMostSignificantBit(LastButtonMask); // Input is a hammer-on and the button pressed is not part of the note mask (incorrect fret) - if (isHammerOn && (ButtonMask & note.NoteMask) == 0) + if (isHammerOn && (EffectiveButtonMask & note.NoteMask) == 0) { return true; } diff --git a/YARG.Core/Engine/Guitar/GuitarEngine.cs b/YARG.Core/Engine/Guitar/GuitarEngine.cs index d39aef29c..1b272d166 100644 --- a/YARG.Core/Engine/Guitar/GuitarEngine.cs +++ b/YARG.Core/Engine/Guitar/GuitarEngine.cs @@ -9,19 +9,18 @@ public abstract class GuitarEngine : BaseEngine { protected const byte OPEN_MASK = 64; + // Mask of all the solo buttons in bit math + protected const ushort SOLO_MASK = 31744; public delegate void OverstrumEvent(); public OverstrumEvent? OnOverstrum; - public byte ButtonMask { get; protected set; } = OPEN_MASK; - + public byte EffectiveButtonMask { get; protected set; } = OPEN_MASK; + public ushort InputButtonMask { get; protected set; } public byte LastButtonMask { get; protected set; } - public byte SoloButtonMask { get; protected set; } = OPEN_MASK; - - // Count of standard buttons currently pressed - public int StandardButtonCount { get; protected set; } = 0; + public bool StandardButtonHeld { get; private set; } protected bool HasFretted; protected bool HasStrummed; @@ -88,9 +87,9 @@ protected override void GenerateQueuedUpdates(double nextTime) public override void Reset(bool keepCurrentButtons = false) { - byte buttons = ButtonMask; + byte buttons = EffectiveButtonMask; - ButtonMask = OPEN_MASK; + EffectiveButtonMask = OPEN_MASK; HasFretted = false; HasStrummed = false; @@ -109,7 +108,7 @@ public override void Reset(bool keepCurrentButtons = false) if (keepCurrentButtons) { - ButtonMask = buttons; + EffectiveButtonMask = buttons; } } @@ -170,7 +169,7 @@ protected override bool CanSustainHold(GuitarNote note) { var mask = note.IsDisjoint ? note.DisjointMask : note.NoteMask; - var buttonsMasked = ButtonMask; + var buttonsMasked = EffectiveButtonMask; if ((mask & OPEN_MASK) != 0) { buttonsMasked |= OPEN_MASK; @@ -363,32 +362,20 @@ protected sealed override int CalculateBaseScore() protected void ToggleFret(int fret, bool active) { - // FIXME: This is a terrible hack - if((int) fret >= 10) { - fret -= 10; - } else { - if (active) - { - // Add one to count of standard buttons pressed, avoiding overflow - StandardButtonCount = (StandardButtonCount < int.MaxValue) ? StandardButtonCount + 1 : StandardButtonCount; - } - else - { - // Subtract one from count, avoiding underflow - StandardButtonCount = (StandardButtonCount > 0) ? StandardButtonCount - 1 : 0; - } - } - ButtonMask = (byte) (active ? ButtonMask | (1 << fret) : ButtonMask & ~(1 << fret)); + InputButtonMask = (ushort) (active ? InputButtonMask | (1 << fret) : InputButtonMask & ~(1 << fret)); + + // What we're doing here is transposing bits 10-14 of the input mask down to 0-4 of the effective mask + // used elsewhere so that solo buttons are treated as if they were regular buttons + byte soloButtonMask = (byte) (InputButtonMask >> 10); + + // EffectiveButtonMask is the bitwise or of the low 8 bits of InputButtonMask and soloButtonMask + EffectiveButtonMask = (byte) (InputButtonMask | soloButtonMask); + StandardButtonHeld = (InputButtonMask & ~OPEN_MASK & ~SOLO_MASK) > 0; } public bool IsFretHeld(GuitarAction fret) { - // FIXME: This is a terrible hack - if((int) fret >= 10) - { - fret -= 10; - } - return (ButtonMask & (1 << (int) fret)) != 0; + return (EffectiveButtonMask & (1 << (int) fret)) != 0; } protected static bool IsFretInput(GameInput input)