From fa11d96f2a95d6aad32bcd8e8ffced15985db8f9 Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 30 Jun 2026 14:39:20 -0400 Subject: [PATCH 1/5] Add autoplay modifier --- scripts/Rhythia.cs | 1 + scripts/game/AutoplayHandler.cs | 477 ++++++++++++++++++ scripts/game/AutoplayHandler.cs.uid | 1 + scripts/game/Runner.cs | 24 + scripts/game/managers/CursorManager.cs | 25 + scripts/game/modifiers/AutoplayModifier.cs | 14 + .../game/modifiers/AutoplayModifier.cs.uid | 1 + scripts/map/Replay.cs | 3 + scripts/util/Misc.cs | 3 + textures/ui/autoplay.png | Bin 0 -> 883 bytes textures/ui/autoplay.png.import | 40 ++ 11 files changed, 589 insertions(+) create mode 100644 scripts/game/AutoplayHandler.cs create mode 100644 scripts/game/AutoplayHandler.cs.uid create mode 100644 scripts/game/modifiers/AutoplayModifier.cs create mode 100644 scripts/game/modifiers/AutoplayModifier.cs.uid create mode 100644 textures/ui/autoplay.png create mode 100644 textures/ui/autoplay.png.import diff --git a/scripts/Rhythia.cs b/scripts/Rhythia.cs index 535dd847..a190dabc 100644 --- a/scripts/Rhythia.cs +++ b/scripts/Rhythia.cs @@ -181,6 +181,7 @@ public static void RegisterModifiers() { Modifiers = [ new NoFailModifier(), + new AutoplayModifier(), new GhostModifier(), new StrobeModifier(), new ChaosModifier(), diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs new file mode 100644 index 00000000..04b2d115 --- /dev/null +++ b/scripts/game/AutoplayHandler.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Godot; + +public class AutoplayHandler +{ + private readonly Attempt attempt; + private readonly List processedData = []; + private int lastLoadedNote; + + private const float MaxShiftMultiplier = 0.25f; + + private readonly struct AutoNote(float x, float y, double millisecond) + { + public readonly float X = x; + public readonly float Y = y; + public readonly double Millisecond = millisecond; + + public Vector2 Position => new(X, Y); + + public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); + + public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); + } + + public AutoplayHandler(Attempt attempt) + { + this.attempt = attempt; + + var notes = attempt.Map.Notes + .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) + .ToList(); + + if (notes.Count == 0) + { + return; + } + + List preprocessedData = initialPreprocess(notes); + List secondaryPreprocessedData = compressStacks(preprocessedData); + shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); + } + + public void Reset(double progress) + { + lastLoadedNote = 0; + + while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) + { + lastLoadedNote++; + } + } + + public Vector2 GetCursorPosition(double progress) + { + if (processedData.Count == 0) + { + return Vector2.Zero; + } + + while (lastLoadedNote + 1 < processedData.Count) + { + AutoNote note = processedData[lastLoadedNote + 1]; + + if (note.Millisecond > progress) + { + break; + } + + lastLoadedNote++; + } + + AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; + AutoNote note1 = processedData[lastLoadedNote]; + AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; + AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private List initialPreprocess(List notes) + { + List preprocessedData = []; + int i = 0; + + while (i < notes.Count) + { + AutoNote note = notes[i]; + i++; + + List collected = [note]; + + while (i < notes.Count) + { + AutoNote next = notes[i]; + + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) + { + collected.Add(next); + i++; + } + else + { + break; + } + } + + Vector2 avgPos = Vector2.Zero; + + foreach (AutoNote collectedNote in collected) + { + avgPos += collectedNote.Position; + } + + avgPos /= collected.Count; + + if (i > 1) + { + AutoNote prevData = preprocessedData[^1]; + Vector2 prevDataPos = prevData.Position; + float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); + + if (timeElapsed > 0) + { + float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); + avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + } + + if (i > 2 && preprocessedData.Count >= 2) + { + AutoNote prevPrevData = preprocessedData[^2]; + Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); + Vector2 dir2 = (avgPos - prevDataPos).Normalized(); + + avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); + } + } + + AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); + AutoNote previousNote = notes[Math.Max(i - 2, 0)]; + + if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) + { + newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); + } + + preprocessedData.Add(newNote); + } + + return preprocessedData; + } + + private List compressStacks(List preprocessedData) + { + if (preprocessedData.Count <= 5) + { + return [.. preprocessedData]; + } + + List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; + int i = 2; + + while (i + 3 < preprocessedData.Count) + { + int stackLength = 1; + AutoNote topNote = preprocessedData[i]; + int topNoteIndex = i; + i++; + + while (i + 2 < preprocessedData.Count) + { + AutoNote nextNote = preprocessedData[i]; + i++; + + if (CheckHit(nextNote, topNote, 0.1f)) + { + stackLength++; + } + else + { + break; + } + } + + i--; + + if (stackLength > 1) + { + AutoNote endNote = preprocessedData[i - 1]; + bool valid = false; + AutoNote validTest = topNote; + List tests = buildStackTests(topNote, endNote); + int checkStart = Math.Max(0, topNoteIndex - 6); + List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); + List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); + List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); + + foreach (AutoNote test in tests) + { + bool currentValid = true; + List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; + + foreach (AutoNote note in notesToCheck) + { + bool anyValid = false; + + for (int offsetTest = 0; offsetTest < 10; offsetTest++) + { + float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); + + if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) + { + anyValid = true; + break; + } + } + + if (!anyValid) + { + currentValid = false; + break; + } + } + + if (currentValid) + { + valid = true; + validTest = test; + break; + } + } + + if (valid) + { + secondaryPreprocessedData.Add(validTest); + } + else + { + secondaryPreprocessedData.Add(topNote); + secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); + secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); + secondaryPreprocessedData.Add(endNote); + } + } + else + { + secondaryPreprocessedData.Add(topNote); + } + } + + secondaryPreprocessedData.Add(preprocessedData[^3]); + secondaryPreprocessedData.Add(preprocessedData[^2]); + secondaryPreprocessedData.Add(preprocessedData[^1]); + + return secondaryPreprocessedData; + } + + private List buildStackTests(AutoNote topNote, AutoNote endNote) + { + List tests = []; + float testWidth = hitboxSize() * 0.5f; + const int testWidthFidelity = 3; + const int testCount = 11; + + for (int i = 0; i < testCount; i++) + { + double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); + tests.Add(new(topNote.X, topNote.Y, millisecond)); + + for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) + { + for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; + tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); + } + } + } + + Vector2 topNotePos = topNote.Position; + tests.Sort((a, b) => + { + int timeCompare = a.Millisecond.CompareTo(b.Millisecond); + return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); + }); + + return tests; + } + + private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) + { + float maxRange = hitboxSize() * MaxShiftMultiplier; + + for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) + { + AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; + AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; + AutoNote note2 = secondaryPreprocessedData[i]; + AutoNote note3 = secondaryPreprocessedData[i + 1]; + AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; + + Vector2 shiftVec; + + if (note2.X == note3.X && note2.Y == note3.Y) + { + Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; + Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; + shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); + } + else + { + Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); + shiftVec = clampShift(pos - note2.Position, maxRange); + } + + bool valid = false; + + for (int testIndex = 0; testIndex < 1; testIndex++) + { + float shiftMulti = (10 - testIndex) / 10.0f; + AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); + List validation = [ + .. processedData.TakeLast(3), + newNote, + .. secondaryPreprocessedData.Skip(i + 1).Take(3) + ]; + + if (validation.Count < 4) + { + processedData.Add(newNote); + valid = true; + break; + } + + bool currentValid = true; + double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; + double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; + + foreach (AutoNote note in originalNotes) + { + if (note.Millisecond >= low && note.Millisecond <= high) + { + bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + + if (!hit) + { + currentValid = false; + break; + } + } + } + + if (currentValid) + { + processedData.Add(newNote); + valid = true; + break; + } + } + + if (!valid) + { + processedData.Add(note2); + } + } + + processedData.Add(preprocessedData[^1]); + } + + private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) + { + if (noteData.Count == 0) + { + return Vector2.Zero; + } + + int tempLastLoadedNote = 0; + + while (tempLastLoadedNote + 1 < noteData.Count) + { + AutoNote note = noteData[tempLastLoadedNote + 1]; + + if (note.Millisecond > elapsed) + { + break; + } + + tempLastLoadedNote++; + } + + AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; + AutoNote note1 = noteData[tempLastLoadedNote]; + AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; + AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); + + private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) + { + Vector2 diff = (notePos.Position - cursorPos).Abs(); + return Math.Max(diff.X, diff.Y) < size; + } + + private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + { + double segmentDuration = note2.Millisecond - note1.Millisecond; + + if (segmentDuration <= 0) + { + return note1.Position; + } + + float u = (float)((time - note1.Millisecond) / segmentDuration); + + return new( + CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + ); + } + + private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) + { + const float splineAlpha = 0.4f; + const float splineTension = -1f; + + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); + + float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + + if (!float.IsFinite(m1)) + { + m1 = 0; + } + + if (!float.IsFinite(m2)) + { + m2 = 0; + } + + float u2 = u * u; + + return (2 * (pos1 - pos2) + m1 + m2) * u2 * u + + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 + + m1 * u + + pos1; + } + + private static Vector2 clampShift(Vector2 shift, float limit) + { + if (shift.X == 0 || shift.Y == 0) + { + return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); + } + + shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; + shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; + + return shift; + } + + private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + + private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); +} diff --git a/scripts/game/AutoplayHandler.cs.uid b/scripts/game/AutoplayHandler.cs.uid new file mode 100644 index 00000000..756374cf --- /dev/null +++ b/scripts/game/AutoplayHandler.cs.uid @@ -0,0 +1 @@ +uid://dx76ccbly75a diff --git a/scripts/game/Runner.cs b/scripts/game/Runner.cs index c7b11711..3bea9da2 100644 --- a/scripts/game/Runner.cs +++ b/scripts/game/Runner.cs @@ -24,9 +24,11 @@ public partial class Runner : Node3D public bool StopQueued = false; private SettingsProfile settings; + private AutoplayHandler autoplayHandler; private double lastFrame = Time.GetTicksUsec(); private bool firstFrame = true; private bool eventsConnected = false; + private bool autoplayEnabled = false; [ExportCategory("Settings")] [Export] public bool NotesOnly = false; @@ -80,6 +82,11 @@ public override void _Process(double delta) // Save replay frame + if (autoplayEnabled) + { + Game.Instance.CursorManager.UpdateAutoplayCursor(autoplayHandler.GetCursorPosition(Attempt.Progress)); + } + // if not paused & record replays on & not a temporary map & time from now and last replay frame was 60 frames apart if (!Attempt.Stopped && settings.RecordReplays && !Attempt.Map.Ephemeral && now - Attempt.LastReplayFrame >= 1000000 / 60) { @@ -240,6 +247,22 @@ public void Play() } } + autoplayEnabled = Attempt.Modifiers.Any(mod => mod is AutoplayModifier) && !Attempt.IsReplay; + + if (autoplayEnabled) + { + foreach (var mod in Attempt.Modifiers.Where(mod => mod is AutoplayModifier)) + { + mod.Activate(Attempt); + } + + autoplayHandler = new(Attempt); + } + else + { + autoplayHandler = null; + } + foreach (var renderer in Renderers) { renderer.Setup(Attempt.Settings, SkinManager.Instance.Skin); @@ -308,6 +331,7 @@ public void Skip() public void Seek(double ms) { Attempt.Progress = ms; + autoplayHandler?.Reset(Attempt.Progress); foreach (var entry in Attempt.Objects) { diff --git a/scripts/game/managers/CursorManager.cs b/scripts/game/managers/CursorManager.cs index 30b15c88..5e1f10c8 100644 --- a/scripts/game/managers/CursorManager.cs +++ b/scripts/game/managers/CursorManager.cs @@ -115,6 +115,31 @@ public void UpdateCursor(Vector2 inputDelta, int cursorIndex = 0) attempt.CameraMode.Process(attempt, replayManager, camera, cursors[cursorIndex], inputDelta, sensitivity); } + public void UpdateAutoplayCursor(Vector2 position) + { + EmitSignalOnCursorUpdated(position); + + var attempt = runner.Attempt; + attempt.RawCursorPosition = position; + attempt.CursorPosition = position.Clamp(-Constants.BOUNDS, Constants.BOUNDS); + + var origin = new Vector3(0, 0, attempt.CameraMode.Name == "Spin" ? 3.5f : 3.75f); + float parallax = (float)settings.CameraParallax; + camera.Position = origin + new Vector3(attempt.CursorPosition.X, attempt.CursorPosition.Y, 0) * parallax; + + if (attempt.CameraMode.Name != "Spin") + { + camera.Rotation = Vector3.Zero; + } + + Vector3 cursorPos = new(attempt.CursorPosition.X, attempt.CursorPosition.Y, 0); + + if (cursorPos.IsFinite()) + { + cursors[0].Position = cursorPos; + } + } + // Reset everything to zero so it doesn't have infinite sensitivity private void repositionAbsolute() { diff --git a/scripts/game/modifiers/AutoplayModifier.cs b/scripts/game/modifiers/AutoplayModifier.cs new file mode 100644 index 00000000..bc77780b --- /dev/null +++ b/scripts/game/modifiers/AutoplayModifier.cs @@ -0,0 +1,14 @@ +using Godot; + +public class AutoplayModifier : Modifier +{ + public override string Name => "Autoplay"; + + public override Color Color => new(0xffd166ff); + + public override void Activate(Attempt attempt) + { + base.Activate(attempt); + attempt.Qualifies = false; + } +} diff --git a/scripts/game/modifiers/AutoplayModifier.cs.uid b/scripts/game/modifiers/AutoplayModifier.cs.uid new file mode 100644 index 00000000..78531d58 --- /dev/null +++ b/scripts/game/modifiers/AutoplayModifier.cs.uid @@ -0,0 +1 @@ +uid://cobkyirweynfj diff --git a/scripts/map/Replay.cs b/scripts/map/Replay.cs index 6da741ea..b3e4cf6b 100644 --- a/scripts/map/Replay.cs +++ b/scripts/map/Replay.cs @@ -129,6 +129,9 @@ public Replay(string path) case "NoFail": Modifiers.Add(new NoFailModifier()); break; + case "Autoplay": + Modifiers.Add(new AutoplayModifier()); + break; case "Ghost": Modifiers.Add(new GhostModifier()); break; diff --git a/scripts/util/Misc.cs b/scripts/util/Misc.cs index 38a1716c..4509f9fe 100644 --- a/scripts/util/Misc.cs +++ b/scripts/util/Misc.cs @@ -17,6 +17,9 @@ public static Texture2D GetModIcon(string mod) case "NoFail": tex = skin.ModNoFailImage; break; + case "Autoplay": + tex = GD.Load("res://textures/ui/autoplay.png"); + break; case "Ghost": tex = skin.ModGhostImage; break; diff --git a/textures/ui/autoplay.png b/textures/ui/autoplay.png new file mode 100644 index 0000000000000000000000000000000000000000..a8c47aca6ac24e8ce5c18ad386081414c5eee2e1 GIT binary patch literal 883 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9GG!XV7ZFl&wk0|T?F zr;B4q#hj*u%{Vrv3nmB zFb1h9i;Z;K)@CMZEDgHlwDV%^i5%gjSE8PN4Av}NKV{X9trIlv^hH)Yk2YEGwC<mR+X&pMGgtRWk@R>+>0PA-Z*#_PA}9qYxS%7q6`PV4z8U#YdBI7Ws` zB6us;JiXk@`7^$Ir5AL^O*uz~=cUsgkCVsx?BD3GO-#L4E)b75x|Ma{-r}&j0%Nge96it5I$|>w& z@%el2!=kQBIcLTHXPnB=e#{aa%l78A@cG}nqP!n&+}j>~Lpqq>PEyXY@5r_EGxuta zr{0-(W9#x0oo!Vfv6kO=SzHNTQpS42Q6r(UvGg_L6BhPkf-B#vZ8`RC#_uVCO?{V| zgAUA*td=f){;mDglV@hnoC6qGfBi4a?o54cskb}CPhd&IEW`TnnbJ(tEzY(tX^>8f zImdcDAiJ44mvzUtf7e|;+i!XwmN4`C|MXyoQnt_hGZ*h(xBi0GsTEVXZs*m0Up7Ny zrSVL@>}bHxro8j)z4*} HQ$iB}za*36 literal 0 HcmV?d00001 diff --git a/textures/ui/autoplay.png.import b/textures/ui/autoplay.png.import new file mode 100644 index 00000000..421958ca --- /dev/null +++ b/textures/ui/autoplay.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pkytlqp7mt2g" +path="res://.godot/imported/autoplay.png-68ce854bc9795d220ef473f6447c55d9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://textures/ui/autoplay.png" +dest_files=["res://.godot/imported/autoplay.png-68ce854bc9795d220ef473f6447c55d9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 From 99e5c9dcc996123197de0c84fedd22657afd41d1 Mon Sep 17 00:00:00 2001 From: cake Date: Thu, 16 Jul 2026 21:54:15 -0400 Subject: [PATCH 2/5] Fix autoplay frame drop misses --- Rhythia.csproj | 2 +- packages.lock.json | 20 +- scripts/game/AutoplayHandler.cs | 932 ++++++++++++++++---------------- scripts/game/Runner.cs | 46 +- 4 files changed, 523 insertions(+), 477 deletions(-) diff --git a/Rhythia.csproj b/Rhythia.csproj index 94da00f6..3f760f66 100644 --- a/Rhythia.csproj +++ b/Rhythia.csproj @@ -1,4 +1,4 @@ - + net10.0 net10.0 diff --git a/packages.lock.json b/packages.lock.json index c2f6a258..8744d50a 100644 --- a/packages.lock.json +++ b/packages.lock.json @@ -19,23 +19,23 @@ }, "Godot.SourceGenerators": { "type": "Direct", - "requested": "[4.6.2, )", - "resolved": "4.6.2", - "contentHash": "CqSn9E38CqCvHARIpZXzYw/KkDKkrGjCYjRrKq3kTAIsDGOOZ/W9wiVnfbvmTpER7gbbrFKAd6WA201ELBynQw==" + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "zPXb65IjRK6NRAq44Rz5AfI9uZc0RkhOMOVzsYuauaWwbsiKsrM0Dtl1t2A90JGbVV4Tk02qVPa3JrR8haFqIw==" }, "GodotSharp": { "type": "Direct", - "requested": "[4.6.2, )", - "resolved": "4.6.2", - "contentHash": "yWYaxRtawrYOQ6TZqKovkw6IN3jNWLTjfrL+pvoGdprvGT7LOYdBCwy0OU7rSfzTn48Rr4IuyuZoSuC0Cp2JeA==" + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "k4VnWoOPueAkTtFoUzhDnP5e4Qs3Gho0ls/0VyHD81F90HeVBvT0YlsGtS5fvkrmxl731d96bKRWNL4+Rd977g==" }, "GodotSharpEditor": { "type": "Direct", - "requested": "[4.6.2, )", - "resolved": "4.6.2", - "contentHash": "WbxolK7BIzSl7d8jhTQQSwZ7JEf+6RHeaZWa6aw7f/qXsN/hXzFSgQvpKxGrLo0icQlowjvF0NomnYO5owMG7w==", + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "jpUfzyAJnG65kWsctfdXKTTPDGzv1eHVlGw/E5Fh5Axw8rGeklQJi0isAX1tBxPjcq/AuELYltpfOdUGc9f0kg==", "dependencies": { - "GodotSharp": "4.6.2" + "GodotSharp": "4.6.0" } }, "Semver": { diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs index 04b2d115..1c53dd1b 100644 --- a/scripts/game/AutoplayHandler.cs +++ b/scripts/game/AutoplayHandler.cs @@ -5,473 +5,475 @@ public class AutoplayHandler { - private readonly Attempt attempt; - private readonly List processedData = []; - private int lastLoadedNote; - - private const float MaxShiftMultiplier = 0.25f; - - private readonly struct AutoNote(float x, float y, double millisecond) - { - public readonly float X = x; - public readonly float Y = y; - public readonly double Millisecond = millisecond; - - public Vector2 Position => new(X, Y); - - public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); - - public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); - } - - public AutoplayHandler(Attempt attempt) - { - this.attempt = attempt; - - var notes = attempt.Map.Notes - .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) - .ToList(); - - if (notes.Count == 0) - { - return; - } - - List preprocessedData = initialPreprocess(notes); - List secondaryPreprocessedData = compressStacks(preprocessedData); - shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); - } - - public void Reset(double progress) - { - lastLoadedNote = 0; - - while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) - { - lastLoadedNote++; - } - } - - public Vector2 GetCursorPosition(double progress) - { - if (processedData.Count == 0) - { - return Vector2.Zero; - } - - while (lastLoadedNote + 1 < processedData.Count) - { - AutoNote note = processedData[lastLoadedNote + 1]; - - if (note.Millisecond > progress) - { - break; - } - - lastLoadedNote++; - } - - AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; - AutoNote note1 = processedData[lastLoadedNote]; - AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; - AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private List initialPreprocess(List notes) - { - List preprocessedData = []; - int i = 0; - - while (i < notes.Count) - { - AutoNote note = notes[i]; - i++; - - List collected = [note]; - - while (i < notes.Count) - { - AutoNote next = notes[i]; - - if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) - { - collected.Add(next); - i++; - } - else - { - break; - } - } - - Vector2 avgPos = Vector2.Zero; - - foreach (AutoNote collectedNote in collected) - { - avgPos += collectedNote.Position; - } - - avgPos /= collected.Count; - - if (i > 1) - { - AutoNote prevData = preprocessedData[^1]; - Vector2 prevDataPos = prevData.Position; - float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); - - if (timeElapsed > 0) - { - float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); - avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; - } - - if (i > 2 && preprocessedData.Count >= 2) - { - AutoNote prevPrevData = preprocessedData[^2]; - Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); - Vector2 dir2 = (avgPos - prevDataPos).Normalized(); - - avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); - } - } - - AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); - AutoNote previousNote = notes[Math.Max(i - 2, 0)]; - - if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) - { - newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); - } - - preprocessedData.Add(newNote); - } - - return preprocessedData; - } - - private List compressStacks(List preprocessedData) - { - if (preprocessedData.Count <= 5) - { - return [.. preprocessedData]; - } - - List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; - int i = 2; - - while (i + 3 < preprocessedData.Count) - { - int stackLength = 1; - AutoNote topNote = preprocessedData[i]; - int topNoteIndex = i; - i++; - - while (i + 2 < preprocessedData.Count) - { - AutoNote nextNote = preprocessedData[i]; - i++; - - if (CheckHit(nextNote, topNote, 0.1f)) - { - stackLength++; - } - else - { - break; - } - } - - i--; - - if (stackLength > 1) - { - AutoNote endNote = preprocessedData[i - 1]; - bool valid = false; - AutoNote validTest = topNote; - List tests = buildStackTests(topNote, endNote); - int checkStart = Math.Max(0, topNoteIndex - 6); - List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); - List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); - List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); - - foreach (AutoNote test in tests) - { - bool currentValid = true; - List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; - - foreach (AutoNote note in notesToCheck) - { - bool anyValid = false; - - for (int offsetTest = 0; offsetTest < 10; offsetTest++) - { - float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); - - if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) - { - anyValid = true; - break; - } - } - - if (!anyValid) - { - currentValid = false; - break; - } - } - - if (currentValid) - { - valid = true; - validTest = test; - break; - } - } - - if (valid) - { - secondaryPreprocessedData.Add(validTest); - } - else - { - secondaryPreprocessedData.Add(topNote); - secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); - secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); - secondaryPreprocessedData.Add(endNote); - } - } - else - { - secondaryPreprocessedData.Add(topNote); - } - } - - secondaryPreprocessedData.Add(preprocessedData[^3]); - secondaryPreprocessedData.Add(preprocessedData[^2]); - secondaryPreprocessedData.Add(preprocessedData[^1]); - - return secondaryPreprocessedData; - } - - private List buildStackTests(AutoNote topNote, AutoNote endNote) - { - List tests = []; - float testWidth = hitboxSize() * 0.5f; - const int testWidthFidelity = 3; - const int testCount = 11; - - for (int i = 0; i < testCount; i++) - { - double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); - tests.Add(new(topNote.X, topNote.Y, millisecond)); - - for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) - { - for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) - { - if (x == 0 && y == 0) - { - continue; - } - - Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; - tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); - } - } - } - - Vector2 topNotePos = topNote.Position; - tests.Sort((a, b) => - { - int timeCompare = a.Millisecond.CompareTo(b.Millisecond); - return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); - }); - - return tests; - } - - private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) - { - float maxRange = hitboxSize() * MaxShiftMultiplier; - - for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) - { - AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; - AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; - AutoNote note2 = secondaryPreprocessedData[i]; - AutoNote note3 = secondaryPreprocessedData[i + 1]; - AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; - - Vector2 shiftVec; - - if (note2.X == note3.X && note2.Y == note3.Y) - { - Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; - Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; - shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); - } - else - { - Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); - shiftVec = clampShift(pos - note2.Position, maxRange); - } - - bool valid = false; - - for (int testIndex = 0; testIndex < 1; testIndex++) - { - float shiftMulti = (10 - testIndex) / 10.0f; - AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); - List validation = [ - .. processedData.TakeLast(3), - newNote, - .. secondaryPreprocessedData.Skip(i + 1).Take(3) - ]; - - if (validation.Count < 4) - { - processedData.Add(newNote); - valid = true; - break; - } - - bool currentValid = true; - double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; - double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; - - foreach (AutoNote note in originalNotes) - { - if (note.Millisecond >= low && note.Millisecond <= high) - { - bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) - || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); - - if (!hit) - { - currentValid = false; - break; - } - } - } - - if (currentValid) - { - processedData.Add(newNote); - valid = true; - break; - } - } - - if (!valid) - { - processedData.Add(note2); - } - } - - processedData.Add(preprocessedData[^1]); - } - - private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) - { - if (noteData.Count == 0) - { - return Vector2.Zero; - } - - int tempLastLoadedNote = 0; - - while (tempLastLoadedNote + 1 < noteData.Count) - { - AutoNote note = noteData[tempLastLoadedNote + 1]; - - if (note.Millisecond > elapsed) - { - break; - } - - tempLastLoadedNote++; - } - - AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; - AutoNote note1 = noteData[tempLastLoadedNote]; - AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; - AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); - - private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) - { - Vector2 diff = (notePos.Position - cursorPos).Abs(); - return Math.Max(diff.X, diff.Y) < size; - } - - private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) - { - double segmentDuration = note2.Millisecond - note1.Millisecond; - - if (segmentDuration <= 0) - { - return note1.Position; - } - - float u = (float)((time - note1.Millisecond) / segmentDuration); - - return new( - CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), - CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) - ); - } - - private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) - { - const float splineAlpha = 0.4f; - const float splineTension = -1f; - - float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); - float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); - float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); - - float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); - float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); - - if (!float.IsFinite(m1)) - { - m1 = 0; - } - - if (!float.IsFinite(m2)) - { - m2 = 0; - } - - float u2 = u * u; - - return (2 * (pos1 - pos2) + m1 + m2) * u2 * u - + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 - + m1 * u - + pos1; - } - - private static Vector2 clampShift(Vector2 shift, float limit) - { - if (shift.X == 0 || shift.Y == 0) - { - return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); - } + private readonly Attempt attempt; + private readonly List processedData = []; + private int lastLoadedNote; + + private const float MaxShiftMultiplier = 0.25f; + + private readonly struct AutoNote(float x, float y, double millisecond) + { + public readonly float X = x; + public readonly float Y = y; + public readonly double Millisecond = millisecond; + + public Vector2 Position => new(X, Y); + + public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); + + public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); + } + + public AutoplayHandler(Attempt attempt) + { + this.attempt = attempt; + + var notes = attempt.Map.Notes + .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) + .ToList(); + + if (notes.Count == 0) + { + return; + } + + // this can be used for difficulty calculation + List preprocessedData = initialPreprocess(notes); + List secondaryPreprocessedData = compressStacks(preprocessedData); + shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); + } + + public void Reset(double progress) + { + lastLoadedNote = 0; + + while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) + { + lastLoadedNote++; + } + } + + public Vector2 GetCursorPosition(double progress) + { + if (processedData.Count == 0) + { + return Vector2.Zero; + } + + while (lastLoadedNote + 1 < processedData.Count) + { + AutoNote note = processedData[lastLoadedNote + 1]; + + if (note.Millisecond > progress) + { + break; + } + + lastLoadedNote++; + } + + AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; + AutoNote note1 = processedData[lastLoadedNote]; + AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; + AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private List initialPreprocess(List notes) + { + List preprocessedData = []; + int i = 0; + + while (i < notes.Count) + { + AutoNote note = notes[i]; + i++; + + List collected = [note]; + + while (i < notes.Count) + { + AutoNote next = notes[i]; + + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) + { + collected.Add(next); + i++; + } + else + { + break; + } + } + + Vector2 avgPos = Vector2.Zero; + + foreach (AutoNote collectedNote in collected) + { + avgPos += collectedNote.Position; + } + + avgPos /= collected.Count; + + if (i > 1) + { + AutoNote prevData = preprocessedData[^1]; + Vector2 prevDataPos = prevData.Position; + float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); + + if (timeElapsed > 0) + { + float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); + avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + } + + if (i > 2 && preprocessedData.Count >= 2) + { + AutoNote prevPrevData = preprocessedData[^2]; + Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); + Vector2 dir2 = (avgPos - prevDataPos).Normalized(); + + avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); + } + } + + AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); + AutoNote previousNote = notes[Math.Max(i - 2, 0)]; + + if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) + { + newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); + } + + preprocessedData.Add(newNote); + } + + return preprocessedData; + } + + private List compressStacks(List preprocessedData) + { + if (preprocessedData.Count <= 5) + { + return [.. preprocessedData]; + } + + List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; + int i = 2; + + while (i + 3 < preprocessedData.Count) + { + int stackLength = 1; + AutoNote topNote = preprocessedData[i]; + int topNoteIndex = i; + i++; + + while (i + 2 < preprocessedData.Count) + { + AutoNote nextNote = preprocessedData[i]; + i++; + + if (CheckHit(nextNote, topNote, 0.1f)) + { + stackLength++; + } + else + { + break; + } + } + + i--; + + if (stackLength > 1) + { + AutoNote endNote = preprocessedData[i - 1]; + bool valid = false; + AutoNote validTest = topNote; + List tests = buildStackTests(topNote, endNote); + int checkStart = Math.Max(0, topNoteIndex - 6); + List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); + List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); + List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); + + foreach (AutoNote test in tests) + { + bool currentValid = true; + List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; + + foreach (AutoNote note in notesToCheck) + { + bool anyValid = false; + + for (int offsetTest = 0; offsetTest < 10; offsetTest++) + { + float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); + + if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) + { + anyValid = true; + break; + } + } + + if (!anyValid) + { + currentValid = false; + break; + } + } + + if (currentValid) + { + valid = true; + validTest = test; + break; + } + } + + if (valid) + { + secondaryPreprocessedData.Add(validTest); + } + else + { + secondaryPreprocessedData.Add(topNote); + secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); + secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); + secondaryPreprocessedData.Add(endNote); + } + } + else + { + secondaryPreprocessedData.Add(topNote); + } + } + + secondaryPreprocessedData.Add(preprocessedData[^3]); + secondaryPreprocessedData.Add(preprocessedData[^2]); + secondaryPreprocessedData.Add(preprocessedData[^1]); + + return secondaryPreprocessedData; + } + + private List buildStackTests(AutoNote topNote, AutoNote endNote) + { + List tests = []; + float testWidth = hitboxSize() * 0.5f; + const int testWidthFidelity = 3; + const int testCount = 11; + + for (int i = 0; i < testCount; i++) + { + double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); + tests.Add(new(topNote.X, topNote.Y, millisecond)); + + for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) + { + for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; + tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); + } + } + } + + Vector2 topNotePos = topNote.Position; + tests.Sort((a, b) => + { + int timeCompare = a.Millisecond.CompareTo(b.Millisecond); + return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); + }); + + return tests; + } + + private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) + { + float maxRange = hitboxSize() * MaxShiftMultiplier; + + for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) + { + // this can be used for difficulty calculation + AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; + AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; + AutoNote note2 = secondaryPreprocessedData[i]; + AutoNote note3 = secondaryPreprocessedData[i + 1]; + AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; + + Vector2 shiftVec; + + if (note2.X == note3.X && note2.Y == note3.Y) + { + Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; + Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; + shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); + } + else + { + Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); + shiftVec = clampShift(pos - note2.Position, maxRange); + } + + bool valid = false; + + for (int testIndex = 0; testIndex < 1; testIndex++) + { + float shiftMulti = (10 - testIndex) / 10.0f; + AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); + List validation = [ + .. processedData.TakeLast(3), + newNote, + .. secondaryPreprocessedData.Skip(i + 1).Take(3) + ]; + + if (validation.Count < 4) + { + processedData.Add(newNote); + valid = true; + break; + } + + bool currentValid = true; + double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; + double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; + + foreach (AutoNote note in originalNotes) + { + if (note.Millisecond >= low && note.Millisecond <= high) + { + bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + + if (!hit) + { + currentValid = false; + break; + } + } + } + + if (currentValid) + { + processedData.Add(newNote); + valid = true; + break; + } + } + + if (!valid) + { + processedData.Add(note2); + } + } + + processedData.Add(preprocessedData[^1]); + } + + private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) + { + if (noteData.Count == 0) + { + return Vector2.Zero; + } + + int tempLastLoadedNote = 0; + + while (tempLastLoadedNote + 1 < noteData.Count) + { + AutoNote note = noteData[tempLastLoadedNote + 1]; + + if (note.Millisecond > elapsed) + { + break; + } + + tempLastLoadedNote++; + } + + AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; + AutoNote note1 = noteData[tempLastLoadedNote]; + AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; + AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); + + private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) + { + Vector2 diff = (notePos.Position - cursorPos).Abs(); + return Math.Max(diff.X, diff.Y) < size; + } + + private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + { + double segmentDuration = note2.Millisecond - note1.Millisecond; + + if (segmentDuration <= 0) + { + return note1.Position; + } + + float u = (float)((time - note1.Millisecond) / segmentDuration); + + return new( + CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + ); + } + + private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) + { + const float splineAlpha = 0.4f; + const float splineTension = -1f; + + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); + + float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + + if (!float.IsFinite(m1)) + { + m1 = 0; + } + + if (!float.IsFinite(m2)) + { + m2 = 0; + } + + float u2 = u * u; + + return (2 * (pos1 - pos2) + m1 + m2) * u2 * u + + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 + + m1 * u + + pos1; + } + + private static Vector2 clampShift(Vector2 shift, float limit) + { + if (shift.X == 0 || shift.Y == 0) + { + return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); + } - shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; - shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; + shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; + shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; - return shift; - } + return shift; + } - private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); - private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); + private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); } diff --git a/scripts/game/Runner.cs b/scripts/game/Runner.cs index 3bea9da2..2ae080bf 100644 --- a/scripts/game/Runner.cs +++ b/scripts/game/Runner.cs @@ -29,6 +29,7 @@ public partial class Runner : Node3D private bool firstFrame = true; private bool eventsConnected = false; private bool autoplayEnabled = false; + private double? hitResultProgressOverride = null; [ExportCategory("Settings")] [Export] public bool NotesOnly = false; @@ -85,6 +86,7 @@ public override void _Process(double delta) if (autoplayEnabled) { Game.Instance.CursorManager.UpdateAutoplayCursor(autoplayHandler.GetCursorPosition(Attempt.Progress)); + ProcessAutoplayHits(); } // if not paused & record replays on & not a temporary map & time from now and last replay frame was 60 frames apart @@ -201,6 +203,47 @@ public void RenderObjects(double delta) } } + private void ProcessAutoplayHits() + { + if (!autoplayEnabled || Attempt.IsReplay || !Attempt.Objects.TryGetValue(typeof(Note), out var objects)) + { + return; + } + + // this can be used for difficulty calculation + int startIndex = ObjectIndicesStart[typeof(Note)]; + + for (int i = startIndex; i < objects.Count; i++) + { + if (objects[i] is not Note note) + { + continue; + } + + if (note.Millisecond > Attempt.Progress) + { + break; + } + + if (note.Millisecond < Attempt.StartFrom || note.LastResult != HitResult.None) + { + continue; + } + + note.Hittable = true; + + try + { + hitResultProgressOverride = note.Millisecond; + note.Hit(this); + } + finally + { + hitResultProgressOverride = null; + } + } + } + public void Play() { if (Attempt == null) return; @@ -465,7 +508,8 @@ public void Stop(bool results = true) private void onHitResultChanged(int noteIndex, HitResult hitResult) { - float lateness = Attempt.IsReplay ? Attempt.HitsInfo[noteIndex] : (float)(((int)Attempt.Progress - Attempt.Map.Notes[noteIndex].Millisecond) / Speed); + double judgmentProgress = hitResultProgressOverride ?? Attempt.Progress; + float lateness = Attempt.IsReplay ? Attempt.HitsInfo[noteIndex] : (float)(((int)judgmentProgress - Attempt.Map.Notes[noteIndex].Millisecond) / Speed); float factor = 1 - Math.Max(0, lateness - 25) / 150f; uint hitScore = (uint)(100 * Attempt.ComboMultiplier * Attempt.ModsMultiplier * factor * ((Speed - 1) / 2.5 + 1)); From 6399a3ca193a472e3a7dda5e696e0035a952e755 Mon Sep 17 00:00:00 2001 From: cake Date: Thu, 16 Jul 2026 21:57:10 -0400 Subject: [PATCH 3/5] Remove accidental project file changes --- Rhythia.csproj | 2 +- packages.lock.json | 20 +- scripts/game/AutoplayHandler.cs | 934 ++++++++++++++++---------------- 3 files changed, 478 insertions(+), 478 deletions(-) diff --git a/Rhythia.csproj b/Rhythia.csproj index 3f760f66..94da00f6 100644 --- a/Rhythia.csproj +++ b/Rhythia.csproj @@ -1,4 +1,4 @@ - + net10.0 net10.0 diff --git a/packages.lock.json b/packages.lock.json index 8744d50a..c2f6a258 100644 --- a/packages.lock.json +++ b/packages.lock.json @@ -19,23 +19,23 @@ }, "Godot.SourceGenerators": { "type": "Direct", - "requested": "[4.6.0, )", - "resolved": "4.6.0", - "contentHash": "zPXb65IjRK6NRAq44Rz5AfI9uZc0RkhOMOVzsYuauaWwbsiKsrM0Dtl1t2A90JGbVV4Tk02qVPa3JrR8haFqIw==" + "requested": "[4.6.2, )", + "resolved": "4.6.2", + "contentHash": "CqSn9E38CqCvHARIpZXzYw/KkDKkrGjCYjRrKq3kTAIsDGOOZ/W9wiVnfbvmTpER7gbbrFKAd6WA201ELBynQw==" }, "GodotSharp": { "type": "Direct", - "requested": "[4.6.0, )", - "resolved": "4.6.0", - "contentHash": "k4VnWoOPueAkTtFoUzhDnP5e4Qs3Gho0ls/0VyHD81F90HeVBvT0YlsGtS5fvkrmxl731d96bKRWNL4+Rd977g==" + "requested": "[4.6.2, )", + "resolved": "4.6.2", + "contentHash": "yWYaxRtawrYOQ6TZqKovkw6IN3jNWLTjfrL+pvoGdprvGT7LOYdBCwy0OU7rSfzTn48Rr4IuyuZoSuC0Cp2JeA==" }, "GodotSharpEditor": { "type": "Direct", - "requested": "[4.6.0, )", - "resolved": "4.6.0", - "contentHash": "jpUfzyAJnG65kWsctfdXKTTPDGzv1eHVlGw/E5Fh5Axw8rGeklQJi0isAX1tBxPjcq/AuELYltpfOdUGc9f0kg==", + "requested": "[4.6.2, )", + "resolved": "4.6.2", + "contentHash": "WbxolK7BIzSl7d8jhTQQSwZ7JEf+6RHeaZWa6aw7f/qXsN/hXzFSgQvpKxGrLo0icQlowjvF0NomnYO5owMG7w==", "dependencies": { - "GodotSharp": "4.6.0" + "GodotSharp": "4.6.2" } }, "Semver": { diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs index 1c53dd1b..7291c818 100644 --- a/scripts/game/AutoplayHandler.cs +++ b/scripts/game/AutoplayHandler.cs @@ -5,475 +5,475 @@ public class AutoplayHandler { - private readonly Attempt attempt; - private readonly List processedData = []; - private int lastLoadedNote; - - private const float MaxShiftMultiplier = 0.25f; - - private readonly struct AutoNote(float x, float y, double millisecond) - { - public readonly float X = x; - public readonly float Y = y; - public readonly double Millisecond = millisecond; - - public Vector2 Position => new(X, Y); - - public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); - - public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); - } - - public AutoplayHandler(Attempt attempt) - { - this.attempt = attempt; - - var notes = attempt.Map.Notes - .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) - .ToList(); - - if (notes.Count == 0) - { - return; - } - - // this can be used for difficulty calculation - List preprocessedData = initialPreprocess(notes); - List secondaryPreprocessedData = compressStacks(preprocessedData); - shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); - } - - public void Reset(double progress) - { - lastLoadedNote = 0; - - while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) - { - lastLoadedNote++; - } - } - - public Vector2 GetCursorPosition(double progress) - { - if (processedData.Count == 0) - { - return Vector2.Zero; - } - - while (lastLoadedNote + 1 < processedData.Count) - { - AutoNote note = processedData[lastLoadedNote + 1]; - - if (note.Millisecond > progress) - { - break; - } - - lastLoadedNote++; - } - - AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; - AutoNote note1 = processedData[lastLoadedNote]; - AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; - AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private List initialPreprocess(List notes) - { - List preprocessedData = []; - int i = 0; - - while (i < notes.Count) - { - AutoNote note = notes[i]; - i++; - - List collected = [note]; - - while (i < notes.Count) - { - AutoNote next = notes[i]; - - if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) - { - collected.Add(next); - i++; - } - else - { - break; - } - } - - Vector2 avgPos = Vector2.Zero; - - foreach (AutoNote collectedNote in collected) - { - avgPos += collectedNote.Position; - } - - avgPos /= collected.Count; - - if (i > 1) - { - AutoNote prevData = preprocessedData[^1]; - Vector2 prevDataPos = prevData.Position; - float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); - - if (timeElapsed > 0) - { - float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); - avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; - } - - if (i > 2 && preprocessedData.Count >= 2) - { - AutoNote prevPrevData = preprocessedData[^2]; - Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); - Vector2 dir2 = (avgPos - prevDataPos).Normalized(); - - avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); - } - } - - AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); - AutoNote previousNote = notes[Math.Max(i - 2, 0)]; - - if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) - { - newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); - } - - preprocessedData.Add(newNote); - } - - return preprocessedData; - } - - private List compressStacks(List preprocessedData) - { - if (preprocessedData.Count <= 5) - { - return [.. preprocessedData]; - } - - List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; - int i = 2; - - while (i + 3 < preprocessedData.Count) - { - int stackLength = 1; - AutoNote topNote = preprocessedData[i]; - int topNoteIndex = i; - i++; - - while (i + 2 < preprocessedData.Count) - { - AutoNote nextNote = preprocessedData[i]; - i++; - - if (CheckHit(nextNote, topNote, 0.1f)) - { - stackLength++; - } - else - { - break; - } - } - - i--; - - if (stackLength > 1) - { - AutoNote endNote = preprocessedData[i - 1]; - bool valid = false; - AutoNote validTest = topNote; - List tests = buildStackTests(topNote, endNote); - int checkStart = Math.Max(0, topNoteIndex - 6); - List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); - List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); - List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); - - foreach (AutoNote test in tests) - { - bool currentValid = true; - List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; - - foreach (AutoNote note in notesToCheck) - { - bool anyValid = false; - - for (int offsetTest = 0; offsetTest < 10; offsetTest++) - { - float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); - - if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) - { - anyValid = true; - break; - } - } - - if (!anyValid) - { - currentValid = false; - break; - } - } - - if (currentValid) - { - valid = true; - validTest = test; - break; - } - } - - if (valid) - { - secondaryPreprocessedData.Add(validTest); - } - else - { - secondaryPreprocessedData.Add(topNote); - secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); - secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); - secondaryPreprocessedData.Add(endNote); - } - } - else - { - secondaryPreprocessedData.Add(topNote); - } - } - - secondaryPreprocessedData.Add(preprocessedData[^3]); - secondaryPreprocessedData.Add(preprocessedData[^2]); - secondaryPreprocessedData.Add(preprocessedData[^1]); - - return secondaryPreprocessedData; - } - - private List buildStackTests(AutoNote topNote, AutoNote endNote) - { - List tests = []; - float testWidth = hitboxSize() * 0.5f; - const int testWidthFidelity = 3; - const int testCount = 11; - - for (int i = 0; i < testCount; i++) - { - double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); - tests.Add(new(topNote.X, topNote.Y, millisecond)); - - for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) - { - for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) - { - if (x == 0 && y == 0) - { - continue; - } - - Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; - tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); - } - } - } - - Vector2 topNotePos = topNote.Position; - tests.Sort((a, b) => - { - int timeCompare = a.Millisecond.CompareTo(b.Millisecond); - return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); - }); - - return tests; - } - - private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) - { - float maxRange = hitboxSize() * MaxShiftMultiplier; - - for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) - { - // this can be used for difficulty calculation - AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; - AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; - AutoNote note2 = secondaryPreprocessedData[i]; - AutoNote note3 = secondaryPreprocessedData[i + 1]; - AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; - - Vector2 shiftVec; - - if (note2.X == note3.X && note2.Y == note3.Y) - { - Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; - Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; - shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); - } - else - { - Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); - shiftVec = clampShift(pos - note2.Position, maxRange); - } - - bool valid = false; - - for (int testIndex = 0; testIndex < 1; testIndex++) - { - float shiftMulti = (10 - testIndex) / 10.0f; - AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); - List validation = [ - .. processedData.TakeLast(3), - newNote, - .. secondaryPreprocessedData.Skip(i + 1).Take(3) - ]; - - if (validation.Count < 4) - { - processedData.Add(newNote); - valid = true; - break; - } - - bool currentValid = true; - double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; - double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; - - foreach (AutoNote note in originalNotes) - { - if (note.Millisecond >= low && note.Millisecond <= high) - { - bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) - || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); - - if (!hit) - { - currentValid = false; - break; - } - } - } - - if (currentValid) - { - processedData.Add(newNote); - valid = true; - break; - } - } - - if (!valid) - { - processedData.Add(note2); - } - } - - processedData.Add(preprocessedData[^1]); - } - - private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) - { - if (noteData.Count == 0) - { - return Vector2.Zero; - } - - int tempLastLoadedNote = 0; - - while (tempLastLoadedNote + 1 < noteData.Count) - { - AutoNote note = noteData[tempLastLoadedNote + 1]; - - if (note.Millisecond > elapsed) - { - break; - } - - tempLastLoadedNote++; - } - - AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; - AutoNote note1 = noteData[tempLastLoadedNote]; - AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; - AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); - - private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) - { - Vector2 diff = (notePos.Position - cursorPos).Abs(); - return Math.Max(diff.X, diff.Y) < size; - } - - private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) - { - double segmentDuration = note2.Millisecond - note1.Millisecond; - - if (segmentDuration <= 0) - { - return note1.Position; - } - - float u = (float)((time - note1.Millisecond) / segmentDuration); - - return new( - CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), - CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) - ); - } - - private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) - { - const float splineAlpha = 0.4f; - const float splineTension = -1f; - - float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); - float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); - float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); - - float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); - float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); - - if (!float.IsFinite(m1)) - { - m1 = 0; - } - - if (!float.IsFinite(m2)) - { - m2 = 0; - } - - float u2 = u * u; - - return (2 * (pos1 - pos2) + m1 + m2) * u2 * u - + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 - + m1 * u - + pos1; - } - - private static Vector2 clampShift(Vector2 shift, float limit) - { - if (shift.X == 0 || shift.Y == 0) - { - return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); - } + private readonly Attempt attempt; + private readonly List processedData = []; + private int lastLoadedNote; + + private const float MaxShiftMultiplier = 0.25f; + + private readonly struct AutoNote(float x, float y, double millisecond) + { + public readonly float X = x; + public readonly float Y = y; + public readonly double Millisecond = millisecond; + + public Vector2 Position => new(X, Y); + + public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); + + public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); + } + + public AutoplayHandler(Attempt attempt) + { + this.attempt = attempt; + + var notes = attempt.Map.Notes + .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) + .ToList(); + + if (notes.Count == 0) + { + return; + } + + // this can be used for difficulty calculation + List preprocessedData = initialPreprocess(notes); + List secondaryPreprocessedData = compressStacks(preprocessedData); + shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); + } + + public void Reset(double progress) + { + lastLoadedNote = 0; + + while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) + { + lastLoadedNote++; + } + } + + public Vector2 GetCursorPosition(double progress) + { + if (processedData.Count == 0) + { + return Vector2.Zero; + } + + while (lastLoadedNote + 1 < processedData.Count) + { + AutoNote note = processedData[lastLoadedNote + 1]; + + if (note.Millisecond > progress) + { + break; + } + + lastLoadedNote++; + } + + AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; + AutoNote note1 = processedData[lastLoadedNote]; + AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; + AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private List initialPreprocess(List notes) + { + List preprocessedData = []; + int i = 0; + + while (i < notes.Count) + { + AutoNote note = notes[i]; + i++; + + List collected = [note]; + + while (i < notes.Count) + { + AutoNote next = notes[i]; + + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) + { + collected.Add(next); + i++; + } + else + { + break; + } + } + + Vector2 avgPos = Vector2.Zero; + + foreach (AutoNote collectedNote in collected) + { + avgPos += collectedNote.Position; + } + + avgPos /= collected.Count; + + if (i > 1) + { + AutoNote prevData = preprocessedData[^1]; + Vector2 prevDataPos = prevData.Position; + float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); + + if (timeElapsed > 0) + { + float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); + avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + } + + if (i > 2 && preprocessedData.Count >= 2) + { + AutoNote prevPrevData = preprocessedData[^2]; + Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); + Vector2 dir2 = (avgPos - prevDataPos).Normalized(); + + avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); + } + } + + AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); + AutoNote previousNote = notes[Math.Max(i - 2, 0)]; + + if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) + { + newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); + } + + preprocessedData.Add(newNote); + } + + return preprocessedData; + } + + private List compressStacks(List preprocessedData) + { + if (preprocessedData.Count <= 5) + { + return [.. preprocessedData]; + } + + List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; + int i = 2; + + while (i + 3 < preprocessedData.Count) + { + int stackLength = 1; + AutoNote topNote = preprocessedData[i]; + int topNoteIndex = i; + i++; + + while (i + 2 < preprocessedData.Count) + { + AutoNote nextNote = preprocessedData[i]; + i++; + + if (CheckHit(nextNote, topNote, 0.1f)) + { + stackLength++; + } + else + { + break; + } + } + + i--; + + if (stackLength > 1) + { + AutoNote endNote = preprocessedData[i - 1]; + bool valid = false; + AutoNote validTest = topNote; + List tests = buildStackTests(topNote, endNote); + int checkStart = Math.Max(0, topNoteIndex - 6); + List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); + List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); + List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); + + foreach (AutoNote test in tests) + { + bool currentValid = true; + List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; + + foreach (AutoNote note in notesToCheck) + { + bool anyValid = false; + + for (int offsetTest = 0; offsetTest < 10; offsetTest++) + { + float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); + + if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) + { + anyValid = true; + break; + } + } + + if (!anyValid) + { + currentValid = false; + break; + } + } + + if (currentValid) + { + valid = true; + validTest = test; + break; + } + } + + if (valid) + { + secondaryPreprocessedData.Add(validTest); + } + else + { + secondaryPreprocessedData.Add(topNote); + secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); + secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); + secondaryPreprocessedData.Add(endNote); + } + } + else + { + secondaryPreprocessedData.Add(topNote); + } + } + + secondaryPreprocessedData.Add(preprocessedData[^3]); + secondaryPreprocessedData.Add(preprocessedData[^2]); + secondaryPreprocessedData.Add(preprocessedData[^1]); + + return secondaryPreprocessedData; + } + + private List buildStackTests(AutoNote topNote, AutoNote endNote) + { + List tests = []; + float testWidth = hitboxSize() * 0.5f; + const int testWidthFidelity = 3; + const int testCount = 11; + + for (int i = 0; i < testCount; i++) + { + double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); + tests.Add(new(topNote.X, topNote.Y, millisecond)); + + for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) + { + for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; + tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); + } + } + } + + Vector2 topNotePos = topNote.Position; + tests.Sort((a, b) => + { + int timeCompare = a.Millisecond.CompareTo(b.Millisecond); + return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); + }); + + return tests; + } + + private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) + { + float maxRange = hitboxSize() * MaxShiftMultiplier; + + for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) + { + // this can be used for difficulty calculation + AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; + AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; + AutoNote note2 = secondaryPreprocessedData[i]; + AutoNote note3 = secondaryPreprocessedData[i + 1]; + AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; + + Vector2 shiftVec; + + if (note2.X == note3.X && note2.Y == note3.Y) + { + Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; + Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; + shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); + } + else + { + Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); + shiftVec = clampShift(pos - note2.Position, maxRange); + } + + bool valid = false; + + for (int testIndex = 0; testIndex < 1; testIndex++) + { + float shiftMulti = (10 - testIndex) / 10.0f; + AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); + List validation = [ + .. processedData.TakeLast(3), + newNote, + .. secondaryPreprocessedData.Skip(i + 1).Take(3) + ]; + + if (validation.Count < 4) + { + processedData.Add(newNote); + valid = true; + break; + } + + bool currentValid = true; + double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; + double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; + + foreach (AutoNote note in originalNotes) + { + if (note.Millisecond >= low && note.Millisecond <= high) + { + bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + + if (!hit) + { + currentValid = false; + break; + } + } + } + + if (currentValid) + { + processedData.Add(newNote); + valid = true; + break; + } + } + + if (!valid) + { + processedData.Add(note2); + } + } + + processedData.Add(preprocessedData[^1]); + } + + private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) + { + if (noteData.Count == 0) + { + return Vector2.Zero; + } + + int tempLastLoadedNote = 0; + + while (tempLastLoadedNote + 1 < noteData.Count) + { + AutoNote note = noteData[tempLastLoadedNote + 1]; + + if (note.Millisecond > elapsed) + { + break; + } + + tempLastLoadedNote++; + } + + AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; + AutoNote note1 = noteData[tempLastLoadedNote]; + AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; + AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); + + private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) + { + Vector2 diff = (notePos.Position - cursorPos).Abs(); + return Math.Max(diff.X, diff.Y) < size; + } + + private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + { + double segmentDuration = note2.Millisecond - note1.Millisecond; + + if (segmentDuration <= 0) + { + return note1.Position; + } + + float u = (float)((time - note1.Millisecond) / segmentDuration); + + return new( + CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + ); + } + + private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) + { + const float splineAlpha = 0.4f; + const float splineTension = -1f; + + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); + + float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + + if (!float.IsFinite(m1)) + { + m1 = 0; + } + + if (!float.IsFinite(m2)) + { + m2 = 0; + } + + float u2 = u * u; + + return (2 * (pos1 - pos2) + m1 + m2) * u2 * u + + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 + + m1 * u + + pos1; + } + + private static Vector2 clampShift(Vector2 shift, float limit) + { + if (shift.X == 0 || shift.Y == 0) + { + return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); + } - shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; - shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; + shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; + shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; - return shift; - } + return shift; + } - private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); - private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); + private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); } From 2c48bd350c9aa0224f0da0c7cfac17269048a48b Mon Sep 17 00:00:00 2001 From: cake Date: Thu, 16 Jul 2026 22:00:46 -0400 Subject: [PATCH 4/5] Apply dotnet format to autoplay handler --- scripts/game/AutoplayHandler.cs | 934 ++++++++++++++++---------------- 1 file changed, 467 insertions(+), 467 deletions(-) diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs index 7291c818..1c53dd1b 100644 --- a/scripts/game/AutoplayHandler.cs +++ b/scripts/game/AutoplayHandler.cs @@ -5,475 +5,475 @@ public class AutoplayHandler { - private readonly Attempt attempt; - private readonly List processedData = []; - private int lastLoadedNote; - - private const float MaxShiftMultiplier = 0.25f; - - private readonly struct AutoNote(float x, float y, double millisecond) - { - public readonly float X = x; - public readonly float Y = y; - public readonly double Millisecond = millisecond; - - public Vector2 Position => new(X, Y); - - public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); - - public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); - } - - public AutoplayHandler(Attempt attempt) - { - this.attempt = attempt; - - var notes = attempt.Map.Notes - .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) - .ToList(); - - if (notes.Count == 0) - { - return; - } - - // this can be used for difficulty calculation - List preprocessedData = initialPreprocess(notes); - List secondaryPreprocessedData = compressStacks(preprocessedData); - shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); - } - - public void Reset(double progress) - { - lastLoadedNote = 0; - - while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) - { - lastLoadedNote++; - } - } - - public Vector2 GetCursorPosition(double progress) - { - if (processedData.Count == 0) - { - return Vector2.Zero; - } - - while (lastLoadedNote + 1 < processedData.Count) - { - AutoNote note = processedData[lastLoadedNote + 1]; - - if (note.Millisecond > progress) - { - break; - } - - lastLoadedNote++; - } - - AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; - AutoNote note1 = processedData[lastLoadedNote]; - AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; - AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private List initialPreprocess(List notes) - { - List preprocessedData = []; - int i = 0; - - while (i < notes.Count) - { - AutoNote note = notes[i]; - i++; - - List collected = [note]; - - while (i < notes.Count) - { - AutoNote next = notes[i]; - - if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) - { - collected.Add(next); - i++; - } - else - { - break; - } - } - - Vector2 avgPos = Vector2.Zero; - - foreach (AutoNote collectedNote in collected) - { - avgPos += collectedNote.Position; - } - - avgPos /= collected.Count; - - if (i > 1) - { - AutoNote prevData = preprocessedData[^1]; - Vector2 prevDataPos = prevData.Position; - float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); - - if (timeElapsed > 0) - { - float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); - avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; - } - - if (i > 2 && preprocessedData.Count >= 2) - { - AutoNote prevPrevData = preprocessedData[^2]; - Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); - Vector2 dir2 = (avgPos - prevDataPos).Normalized(); - - avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); - } - } - - AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); - AutoNote previousNote = notes[Math.Max(i - 2, 0)]; - - if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) - { - newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); - } - - preprocessedData.Add(newNote); - } - - return preprocessedData; - } - - private List compressStacks(List preprocessedData) - { - if (preprocessedData.Count <= 5) - { - return [.. preprocessedData]; - } - - List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; - int i = 2; - - while (i + 3 < preprocessedData.Count) - { - int stackLength = 1; - AutoNote topNote = preprocessedData[i]; - int topNoteIndex = i; - i++; - - while (i + 2 < preprocessedData.Count) - { - AutoNote nextNote = preprocessedData[i]; - i++; - - if (CheckHit(nextNote, topNote, 0.1f)) - { - stackLength++; - } - else - { - break; - } - } - - i--; - - if (stackLength > 1) - { - AutoNote endNote = preprocessedData[i - 1]; - bool valid = false; - AutoNote validTest = topNote; - List tests = buildStackTests(topNote, endNote); - int checkStart = Math.Max(0, topNoteIndex - 6); - List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); - List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); - List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); - - foreach (AutoNote test in tests) - { - bool currentValid = true; - List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; - - foreach (AutoNote note in notesToCheck) - { - bool anyValid = false; - - for (int offsetTest = 0; offsetTest < 10; offsetTest++) - { - float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); - - if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) - { - anyValid = true; - break; - } - } - - if (!anyValid) - { - currentValid = false; - break; - } - } - - if (currentValid) - { - valid = true; - validTest = test; - break; - } - } - - if (valid) - { - secondaryPreprocessedData.Add(validTest); - } - else - { - secondaryPreprocessedData.Add(topNote); - secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); - secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); - secondaryPreprocessedData.Add(endNote); - } - } - else - { - secondaryPreprocessedData.Add(topNote); - } - } - - secondaryPreprocessedData.Add(preprocessedData[^3]); - secondaryPreprocessedData.Add(preprocessedData[^2]); - secondaryPreprocessedData.Add(preprocessedData[^1]); - - return secondaryPreprocessedData; - } - - private List buildStackTests(AutoNote topNote, AutoNote endNote) - { - List tests = []; - float testWidth = hitboxSize() * 0.5f; - const int testWidthFidelity = 3; - const int testCount = 11; - - for (int i = 0; i < testCount; i++) - { - double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); - tests.Add(new(topNote.X, topNote.Y, millisecond)); - - for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) - { - for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) - { - if (x == 0 && y == 0) - { - continue; - } - - Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; - tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); - } - } - } - - Vector2 topNotePos = topNote.Position; - tests.Sort((a, b) => - { - int timeCompare = a.Millisecond.CompareTo(b.Millisecond); - return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); - }); - - return tests; - } - - private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) - { - float maxRange = hitboxSize() * MaxShiftMultiplier; - - for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) - { - // this can be used for difficulty calculation - AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; - AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; - AutoNote note2 = secondaryPreprocessedData[i]; - AutoNote note3 = secondaryPreprocessedData[i + 1]; - AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; - - Vector2 shiftVec; - - if (note2.X == note3.X && note2.Y == note3.Y) - { - Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; - Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; - shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); - } - else - { - Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); - shiftVec = clampShift(pos - note2.Position, maxRange); - } - - bool valid = false; - - for (int testIndex = 0; testIndex < 1; testIndex++) - { - float shiftMulti = (10 - testIndex) / 10.0f; - AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); - List validation = [ - .. processedData.TakeLast(3), - newNote, - .. secondaryPreprocessedData.Skip(i + 1).Take(3) - ]; - - if (validation.Count < 4) - { - processedData.Add(newNote); - valid = true; - break; - } - - bool currentValid = true; - double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; - double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; - - foreach (AutoNote note in originalNotes) - { - if (note.Millisecond >= low && note.Millisecond <= high) - { - bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) - || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); - - if (!hit) - { - currentValid = false; - break; - } - } - } - - if (currentValid) - { - processedData.Add(newNote); - valid = true; - break; - } - } - - if (!valid) - { - processedData.Add(note2); - } - } - - processedData.Add(preprocessedData[^1]); - } - - private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) - { - if (noteData.Count == 0) - { - return Vector2.Zero; - } - - int tempLastLoadedNote = 0; - - while (tempLastLoadedNote + 1 < noteData.Count) - { - AutoNote note = noteData[tempLastLoadedNote + 1]; - - if (note.Millisecond > elapsed) - { - break; - } - - tempLastLoadedNote++; - } - - AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; - AutoNote note1 = noteData[tempLastLoadedNote]; - AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; - AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; - - return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); - } - - private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); - - private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) - { - Vector2 diff = (notePos.Position - cursorPos).Abs(); - return Math.Max(diff.X, diff.Y) < size; - } - - private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) - { - double segmentDuration = note2.Millisecond - note1.Millisecond; - - if (segmentDuration <= 0) - { - return note1.Position; - } - - float u = (float)((time - note1.Millisecond) / segmentDuration); - - return new( - CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), - CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) - ); - } - - private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) - { - const float splineAlpha = 0.4f; - const float splineTension = -1f; - - float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); - float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); - float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); - - float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); - float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); - - if (!float.IsFinite(m1)) - { - m1 = 0; - } - - if (!float.IsFinite(m2)) - { - m2 = 0; - } - - float u2 = u * u; - - return (2 * (pos1 - pos2) + m1 + m2) * u2 * u - + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 - + m1 * u - + pos1; - } - - private static Vector2 clampShift(Vector2 shift, float limit) - { - if (shift.X == 0 || shift.Y == 0) - { - return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); - } + private readonly Attempt attempt; + private readonly List processedData = []; + private int lastLoadedNote; + + private const float MaxShiftMultiplier = 0.25f; + + private readonly struct AutoNote(float x, float y, double millisecond) + { + public readonly float X = x; + public readonly float Y = y; + public readonly double Millisecond = millisecond; + + public Vector2 Position => new(X, Y); + + public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); + + public AutoNote WithMillisecond(double millisecond) => new(X, Y, millisecond); + } + + public AutoplayHandler(Attempt attempt) + { + this.attempt = attempt; + + var notes = attempt.Map.Notes + .Select(note => new AutoNote(note.X, note.Y, note.Millisecond)) + .ToList(); + + if (notes.Count == 0) + { + return; + } + + // this can be used for difficulty calculation + List preprocessedData = initialPreprocess(notes); + List secondaryPreprocessedData = compressStacks(preprocessedData); + shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); + } + + public void Reset(double progress) + { + lastLoadedNote = 0; + + while (lastLoadedNote + 1 < processedData.Count && processedData[lastLoadedNote + 1].Millisecond <= progress) + { + lastLoadedNote++; + } + } + + public Vector2 GetCursorPosition(double progress) + { + if (processedData.Count == 0) + { + return Vector2.Zero; + } + + while (lastLoadedNote + 1 < processedData.Count) + { + AutoNote note = processedData[lastLoadedNote + 1]; + + if (note.Millisecond > progress) + { + break; + } + + lastLoadedNote++; + } + + AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; + AutoNote note1 = processedData[lastLoadedNote]; + AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; + AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private List initialPreprocess(List notes) + { + List preprocessedData = []; + int i = 0; + + while (i < notes.Count) + { + AutoNote note = notes[i]; + i++; + + List collected = [note]; + + while (i < notes.Count) + { + AutoNote next = notes[i]; + + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) + { + collected.Add(next); + i++; + } + else + { + break; + } + } + + Vector2 avgPos = Vector2.Zero; + + foreach (AutoNote collectedNote in collected) + { + avgPos += collectedNote.Position; + } + + avgPos /= collected.Count; + + if (i > 1) + { + AutoNote prevData = preprocessedData[^1]; + Vector2 prevDataPos = prevData.Position; + float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); + + if (timeElapsed > 0) + { + float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); + avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + } + + if (i > 2 && preprocessedData.Count >= 2) + { + AutoNote prevPrevData = preprocessedData[^2]; + Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); + Vector2 dir2 = (avgPos - prevDataPos).Normalized(); + + avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); + } + } + + AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); + AutoNote previousNote = notes[Math.Max(i - 2, 0)]; + + if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) + { + newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); + } + + preprocessedData.Add(newNote); + } + + return preprocessedData; + } + + private List compressStacks(List preprocessedData) + { + if (preprocessedData.Count <= 5) + { + return [.. preprocessedData]; + } + + List secondaryPreprocessedData = [preprocessedData[0], preprocessedData[1]]; + int i = 2; + + while (i + 3 < preprocessedData.Count) + { + int stackLength = 1; + AutoNote topNote = preprocessedData[i]; + int topNoteIndex = i; + i++; + + while (i + 2 < preprocessedData.Count) + { + AutoNote nextNote = preprocessedData[i]; + i++; + + if (CheckHit(nextNote, topNote, 0.1f)) + { + stackLength++; + } + else + { + break; + } + } + + i--; + + if (stackLength > 1) + { + AutoNote endNote = preprocessedData[i - 1]; + bool valid = false; + AutoNote validTest = topNote; + List tests = buildStackTests(topNote, endNote); + int checkStart = Math.Max(0, topNoteIndex - 6); + List notesToCheck = preprocessedData.GetRange(checkStart, Math.Min(i + 6, preprocessedData.Count) - checkStart); + List cursorPositionNotes = secondaryPreprocessedData.GetRange(Math.Max(0, secondaryPreprocessedData.Count - stackLength - 12), Math.Min(stackLength + 12, secondaryPreprocessedData.Count)); + List cursorPositionNotesEnd = preprocessedData.GetRange(i, Math.Min(12, preprocessedData.Count - i)); + + foreach (AutoNote test in tests) + { + bool currentValid = true; + List validation = [.. cursorPositionNotes, test, .. cursorPositionNotesEnd]; + + foreach (AutoNote note in notesToCheck) + { + bool anyValid = false; + + for (int offsetTest = 0; offsetTest < 10; offsetTest++) + { + float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); + + if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) + { + anyValid = true; + break; + } + } + + if (!anyValid) + { + currentValid = false; + break; + } + } + + if (currentValid) + { + valid = true; + validTest = test; + break; + } + } + + if (valid) + { + secondaryPreprocessedData.Add(validTest); + } + else + { + secondaryPreprocessedData.Add(topNote); + secondaryPreprocessedData.Add(topNote.WithMillisecond(topNote.Millisecond + 10)); + secondaryPreprocessedData.Add(endNote.WithMillisecond(endNote.Millisecond - 10)); + secondaryPreprocessedData.Add(endNote); + } + } + else + { + secondaryPreprocessedData.Add(topNote); + } + } + + secondaryPreprocessedData.Add(preprocessedData[^3]); + secondaryPreprocessedData.Add(preprocessedData[^2]); + secondaryPreprocessedData.Add(preprocessedData[^1]); + + return secondaryPreprocessedData; + } + + private List buildStackTests(AutoNote topNote, AutoNote endNote) + { + List tests = []; + float testWidth = hitboxSize() * 0.5f; + const int testWidthFidelity = 3; + const int testCount = 11; + + for (int i = 0; i < testCount; i++) + { + double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); + tests.Add(new(topNote.X, topNote.Y, millisecond)); + + for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) + { + for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; + tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); + } + } + } + + Vector2 topNotePos = topNote.Position; + tests.Sort((a, b) => + { + int timeCompare = a.Millisecond.CompareTo(b.Millisecond); + return timeCompare != 0 ? timeCompare : a.Position.DistanceSquaredTo(topNotePos).CompareTo(b.Position.DistanceSquaredTo(topNotePos)); + }); + + return tests; + } + + private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) + { + float maxRange = hitboxSize() * MaxShiftMultiplier; + + for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) + { + // this can be used for difficulty calculation + AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; + AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; + AutoNote note2 = secondaryPreprocessedData[i]; + AutoNote note3 = secondaryPreprocessedData[i + 1]; + AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; + + Vector2 shiftVec; + + if (note2.X == note3.X && note2.Y == note3.Y) + { + Vector2 previous = processedData.Count > 0 ? processedData[^1].Position : note1.Position; + Vector2 desired = (previous + note2.Position * 0.5f + note3.Position) / 2.5f; + shiftVec = clampShift(desired - note2.Position, hitboxSize() * 0.75f); + } + else + { + Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); + shiftVec = clampShift(pos - note2.Position, maxRange); + } + + bool valid = false; + + for (int testIndex = 0; testIndex < 1; testIndex++) + { + float shiftMulti = (10 - testIndex) / 10.0f; + AutoNote newNote = note2.WithPosition(note2.Position + shiftVec * shiftMulti); + List validation = [ + .. processedData.TakeLast(3), + newNote, + .. secondaryPreprocessedData.Skip(i + 1).Take(3) + ]; + + if (validation.Count < 4) + { + processedData.Add(newNote); + valid = true; + break; + } + + bool currentValid = true; + double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; + double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; + + foreach (AutoNote note in originalNotes) + { + if (note.Millisecond >= low && note.Millisecond <= high) + { + bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + + if (!hit) + { + currentValid = false; + break; + } + } + } + + if (currentValid) + { + processedData.Add(newNote); + valid = true; + break; + } + } + + if (!valid) + { + processedData.Add(note2); + } + } + + processedData.Add(preprocessedData[^1]); + } + + private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) + { + if (noteData.Count == 0) + { + return Vector2.Zero; + } + + int tempLastLoadedNote = 0; + + while (tempLastLoadedNote + 1 < noteData.Count) + { + AutoNote note = noteData[tempLastLoadedNote + 1]; + + if (note.Millisecond > elapsed) + { + break; + } + + tempLastLoadedNote++; + } + + AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; + AutoNote note1 = noteData[tempLastLoadedNote]; + AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; + AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; + + return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); + + private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) + { + Vector2 diff = (notePos.Position - cursorPos).Abs(); + return Math.Max(diff.X, diff.Y) < size; + } + + private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + { + double segmentDuration = note2.Millisecond - note1.Millisecond; + + if (segmentDuration <= 0) + { + return note1.Position; + } + + float u = (float)((time - note1.Millisecond) / segmentDuration); + + return new( + CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + ); + } + + private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) + { + const float splineAlpha = 0.4f; + const float splineTension = -1f; + + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); + + float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + + if (!float.IsFinite(m1)) + { + m1 = 0; + } + + if (!float.IsFinite(m2)) + { + m2 = 0; + } + + float u2 = u * u; + + return (2 * (pos1 - pos2) + m1 + m2) * u2 * u + + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 + + m1 * u + + pos1; + } + + private static Vector2 clampShift(Vector2 shift, float limit) + { + if (shift.X == 0 || shift.Y == 0) + { + return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); + } - shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; - shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; + shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; + shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; - return shift; - } + return shift; + } - private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); - private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); + private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); } From 701af00481dd3dce96a2e7774b7e0708845c8a7c Mon Sep 17 00:00:00 2001 From: cake Date: Thu, 16 Jul 2026 22:05:36 -0400 Subject: [PATCH 5/5] Fix autoplay naming style --- scripts/game/AutoplayHandler.cs | 66 ++++++++++++++++----------------- scripts/game/Runner.cs | 4 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs index 1c53dd1b..dd786446 100644 --- a/scripts/game/AutoplayHandler.cs +++ b/scripts/game/AutoplayHandler.cs @@ -9,7 +9,7 @@ public class AutoplayHandler private readonly List processedData = []; private int lastLoadedNote; - private const float MaxShiftMultiplier = 0.25f; + private const float max_shift_multiplier = 0.25f; private readonly struct AutoNote(float x, float y, double millisecond) { @@ -77,7 +77,7 @@ public Vector2 GetCursorPosition(double progress) AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; - return GetSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + return getSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); } private List initialPreprocess(List notes) @@ -96,7 +96,7 @@ private List initialPreprocess(List notes) { AutoNote next = notes[i]; - if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && CheckHit(note, next, hitboxSize() * 0.9f)) + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && checkHit(note, next, hitboxSize() * 0.9f)) { collected.Add(next); i++; @@ -125,7 +125,7 @@ private List initialPreprocess(List notes) if (timeElapsed > 0) { float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); - avgPos *= 0.8f + (Sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + avgPos *= 0.8f + (sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; } if (i > 2 && preprocessedData.Count >= 2) @@ -174,7 +174,7 @@ private List compressStacks(List preprocessedData) AutoNote nextNote = preprocessedData[i]; i++; - if (CheckHit(nextNote, topNote, 0.1f)) + if (checkHit(nextNote, topNote, 0.1f)) { stackLength++; } @@ -210,7 +210,7 @@ private List compressStacks(List preprocessedData) { float offsetCheck = Mathf.Lerp(5, (float)Constants.HIT_WINDOW * 0.8f, 1 - offsetTest / 9.0f); - if (CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) + if (checkHit(note, getCursorPositionFromNotes(validation, note.Millisecond + offsetCheck), hitboxSize() * 0.74f)) { anyValid = true; break; @@ -261,24 +261,24 @@ private List buildStackTests(AutoNote topNote, AutoNote endNote) { List tests = []; float testWidth = hitboxSize() * 0.5f; - const int testWidthFidelity = 3; - const int testCount = 11; + const int test_width_fidelity = 3; + const int test_count = 11; - for (int i = 0; i < testCount; i++) + for (int i = 0; i < test_count; i++) { - double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (testCount - 1.0f)); + double millisecond = Mathf.Lerp(topNote.Millisecond, endNote.Millisecond, i / (test_count - 1.0f)); tests.Add(new(topNote.X, topNote.Y, millisecond)); - for (int x = -testWidthFidelity; x <= testWidthFidelity; x++) + for (int x = -test_width_fidelity; x <= test_width_fidelity; x++) { - for (int y = -testWidthFidelity; y <= testWidthFidelity; y++) + for (int y = -test_width_fidelity; y <= test_width_fidelity; y++) { if (x == 0 && y == 0) { continue; } - Vector2 offset = new Vector2(x, y) / testWidthFidelity * testWidth; + Vector2 offset = new Vector2(x, y) / test_width_fidelity * testWidth; tests.Add(new(topNote.X + offset.X, topNote.Y + offset.Y, millisecond)); } } @@ -296,7 +296,7 @@ private List buildStackTests(AutoNote topNote, AutoNote endNote) private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) { - float maxRange = hitboxSize() * MaxShiftMultiplier; + float maxRange = hitboxSize() * max_shift_multiplier; for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) { @@ -317,7 +317,7 @@ private void shiftPreprocess(List originalNotes, List prepro } else { - Vector2 pos = GetSplinePosition(note0, note1, note3, note4, note2.Millisecond); + Vector2 pos = getSplinePosition(note0, note1, note3, note4, note2.Millisecond); shiftVec = clampShift(pos - note2.Position, maxRange); } @@ -348,8 +348,8 @@ .. secondaryPreprocessedData.Skip(i + 1).Take(3) { if (note.Millisecond >= low && note.Millisecond <= high) { - bool hit = CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) - || CheckHit(note, GetCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + bool hit = checkHit(note, getCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || checkHit(note, getCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); if (!hit) { @@ -376,7 +376,7 @@ .. secondaryPreprocessedData.Skip(i + 1).Take(3) processedData.Add(preprocessedData[^1]); } - private Vector2 GetCursorPositionFromNotes(List noteData, double elapsed) + private Vector2 getCursorPositionFromNotes(List noteData, double elapsed) { if (noteData.Count == 0) { @@ -402,18 +402,18 @@ private Vector2 GetCursorPositionFromNotes(List noteData, double elaps AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; - return GetSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + return getSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); } - private static bool CheckHit(AutoNote notePos, AutoNote cursorPos, float size) => CheckHit(notePos, cursorPos.Position, size); + private static bool checkHit(AutoNote notePos, AutoNote cursorPos, float size) => checkHit(notePos, cursorPos.Position, size); - private static bool CheckHit(AutoNote notePos, Vector2 cursorPos, float size) + private static bool checkHit(AutoNote notePos, Vector2 cursorPos, float size) { Vector2 diff = (notePos.Position - cursorPos).Abs(); return Math.Max(diff.X, diff.Y) < size; } - private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + private static Vector2 getSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) { double segmentDuration = note2.Millisecond - note1.Millisecond; @@ -425,22 +425,22 @@ private static Vector2 GetSplinePosition(AutoNote note0, AutoNote note1, AutoNot float u = (float)((time - note1.Millisecond) / segmentDuration); return new( - CatmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), - CatmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + catmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + catmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) ); } - private static float CatmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) + private static float catmullRomRaw(float pos0, float pos1, float pos2, float pos3, float u, double time0, double time1, double time2, double time3) { - const float splineAlpha = 0.4f; - const float splineTension = -1f; + const float spline_alpha = 0.4f; + const float spline_tension = -1f; - float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), splineAlpha); - float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), splineAlpha); - float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), splineAlpha); + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), spline_alpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), spline_alpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), spline_alpha); - float m1 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); - float m2 = (1 - splineTension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + float m1 = (1 - spline_tension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - spline_tension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); if (!float.IsFinite(m1)) { @@ -473,7 +473,7 @@ private static Vector2 clampShift(Vector2 shift, float limit) return shift; } - private static float Sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + private static float sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); } diff --git a/scripts/game/Runner.cs b/scripts/game/Runner.cs index 2ae080bf..a27021ed 100644 --- a/scripts/game/Runner.cs +++ b/scripts/game/Runner.cs @@ -86,7 +86,7 @@ public override void _Process(double delta) if (autoplayEnabled) { Game.Instance.CursorManager.UpdateAutoplayCursor(autoplayHandler.GetCursorPosition(Attempt.Progress)); - ProcessAutoplayHits(); + processAutoplayHits(); } // if not paused & record replays on & not a temporary map & time from now and last replay frame was 60 frames apart @@ -203,7 +203,7 @@ public void RenderObjects(double delta) } } - private void ProcessAutoplayHits() + private void processAutoplayHits() { if (!autoplayEnabled || Attempt.IsReplay || !Attempt.Objects.TryGetValue(typeof(Note), out var objects)) {