using System.Diagnostics; using ProjectQuint.Core.Judgement; namespace ProjectQuint.Probes; public readonly record struct JudgementSnapshot(int Score, int Combo, int MaxCombo, int MissCount, int KoolCount, int CoolCount, int GoodCount, int JudgedUnits, bool Finished); public sealed record ReplayObservation(JudgementSnapshot Judgement, double SetupMilliseconds, double ReplayMilliseconds, long SetupAllocatedBytes, long ReplayAllocatedBytes); public static class ReplayRunner { public static ReplayObservation Run(SyntheticWorkload workload, int frameRate) { if (frameRate < 1 || frameRate > 10_000) throw new ArgumentOutOfRangeException(nameof(frameRate), "Use a frame rate from 1 to 10000."); var allocationStart = GC.GetAllocatedBytesForCurrentThread(); var setupStart = Stopwatch.GetTimestamp(); var engine = new RhythmJudgementEngine(workload.Notes, laneCount: 4); var setupEnd = Stopwatch.GetTimestamp(); var replayAllocationStart = GC.GetAllocatedBytesForCurrentThread(); long frame = 0; foreach (var input in workload.Events) { // Merge two timestamped streams; no sleeps or real-time waiting affect this trace. while ((double)frame / frameRate <= input.Time) engine.Update((double)frame++ / frameRate); if (input.Pressed) engine.Press(input.Lane, input.Time); else engine.Release(input.Lane, input.Time); } engine.Update(workload.Notes[^1].EndTime + 1); var replayEnd = Stopwatch.GetTimestamp(); var allocationEnd = GC.GetAllocatedBytesForCurrentThread(); return new ReplayObservation(new JudgementSnapshot(engine.Score, engine.Combo, engine.MaxCombo, engine.MissCount, engine.KoolCount, engine.CoolCount, engine.GoodCount, engine.JudgedNoteCount, engine.IsFinished), Milliseconds(setupStart, setupEnd), Milliseconds(setupEnd, replayEnd), replayAllocationStart - allocationStart, allocationEnd - replayAllocationStart); } private static double Milliseconds(long start, long end) => (end - start) * 1000.0 / Stopwatch.Frequency; }