From 1f7f384e9161c926e5e366290a3701065c8f1653 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 4 Feb 2025 17:29:34 +0100 Subject: [PATCH] feat: Add save-state support for cycle commands --- .../Source/ModInterop/SpeedrunToolInterop.cs | 6 ++++ .../TAS/Input/Commands/CycleCommands.cs | 28 ++++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CelesteTAS-EverestInterop/Source/ModInterop/SpeedrunToolInterop.cs b/CelesteTAS-EverestInterop/Source/ModInterop/SpeedrunToolInterop.cs index 80705c58..4d279352 100644 --- a/CelesteTAS-EverestInterop/Source/ModInterop/SpeedrunToolInterop.cs +++ b/CelesteTAS-EverestInterop/Source/ModInterop/SpeedrunToolInterop.cs @@ -38,6 +38,7 @@ public static class SpeedrunToolInterop { private static bool disallowUnsafeInput; private static Random auraRandom; private static bool betterInvincible = false; + private static Dictionary? cycleData; [Load] private static void Load() { @@ -64,6 +65,7 @@ public static void AddSaveLoadAction() { disallowUnsafeInput = SafeCommand.DisallowUnsafeInput; auraRandom = DesyncFixer.AuraHelperSharedRandom.DeepCloneShared(); betterInvincible = Manager.Running && BetterInvincible.Invincible; + cycleData = CycleCommands.CycleData.DeepCloneShared(); }; Action>, Level> load = (_, _) => { EntityDataHelper.CachedEntityData = savedEntityData.DeepCloneShared(); @@ -86,6 +88,10 @@ public static void AddSaveLoadAction() { SafeCommand.DisallowUnsafeInput = disallowUnsafeInput; DesyncFixer.AuraHelperSharedRandom = auraRandom.DeepCloneShared(); BetterInvincible.Invincible = Manager.Running && betterInvincible; + CycleCommands.CycleData.Clear(); + foreach ((string key, var value) in cycleData) { + CycleCommands.CycleData[key] = value.DeepCloneShared(); + } }; Action clear = () => { savedEntityData = null; diff --git a/CelesteTAS-EverestInterop/Source/TAS/Input/Commands/CycleCommands.cs b/CelesteTAS-EverestInterop/Source/TAS/Input/Commands/CycleCommands.cs index 4fda6272..8ee20a69 100644 --- a/CelesteTAS-EverestInterop/Source/TAS/Input/Commands/CycleCommands.cs +++ b/CelesteTAS-EverestInterop/Source/TAS/Input/Commands/CycleCommands.cs @@ -32,18 +32,18 @@ private enum CycleType { TimeActiveInterval } // The delta time of every frame between a WaitCycle and RequireCycle needs to be tracked, // to accurately calculate the required wait frames for conditions using TimeActive - private static readonly Dictionary cycleData = new(); + internal static readonly Dictionary CycleData = new(); [ClearInputs] private static void ClearInputs() { waitCycles.Clear(); - cycleData.Clear(); + CycleData.Clear(); } [ParseFileEnd] private static void ParseFileEnd() { foreach (string cycle in waitCycles.Keys) { - if (!cycleData.ContainsKey(cycle)) { + if (!CycleData.ContainsKey(cycle)) { // TODO: Display path / line of mismatching WaitCycle command AbortTas("No matching RequireCycle for WaitCycle"); } @@ -54,12 +54,12 @@ private static void ParseFileEnd() { private static void PostSceneUpdate(Scene scene) { var controller = Manager.Controller; - foreach (string cycle in cycleData.Keys) { - var data = cycleData[cycle]; + foreach (string cycle in CycleData.Keys) { + var data = CycleData[cycle]; int startFrame = waitCycles[cycle]; int duration = data.DeltaTimes.Length; - if (controller.CurrentFrameInTas < startFrame || controller.CurrentFrameInTas >= startFrame + duration) { + if (controller.CurrentFrameInTas < startFrame || controller.CurrentFrameInTas >= startFrame + duration || data.CurrentIndex >= duration) { continue; } @@ -70,12 +70,14 @@ private static void PostSceneUpdate(Scene scene) { data.DeltaTimes[data.CurrentIndex++] = Engine.DeltaTime; - cycleData[cycle] = data; + CycleData[cycle] = data; } } - // WaitCycle, Name, Input - [TasCommand("WaitCycle", ExecuteTiming = ExecuteTiming.Parse, MetaDataProvider = typeof(WaitMeta))] + /// WaitCycle, Name, Input + /// + /// Exclude from checksum, to avoid a cycle change causing a save-state clear + [TasCommand("WaitCycle", ExecuteTiming = ExecuteTiming.Parse, CalcChecksum = false, MetaDataProvider = typeof(WaitMeta))] private static void WaitCycle(CommandLine commandLine, int studioLine, string filePath, int fileLine) { if (commandLine.Arguments.Length < 1) { AbortTas("Expected cycle name"); @@ -101,7 +103,7 @@ private static void WaitCycle(CommandLine commandLine, int studioLine, string fi } } - // RequireCycle, Name, TimeActiveInterval, Interval, Offset + /// RequireCycle, Name, TimeActiveInterval, Interval, Offset [TasCommand("RequireCycle", ExecuteTiming = ExecuteTiming.Parse | ExecuteTiming.Runtime, MetaDataProvider = typeof(RequireMeta))] private static void RequireCycle(CommandLine commandLine, int studioLine, string filePath, int fileLine) { switch (commandLine.Arguments.Length) { @@ -122,12 +124,12 @@ private static void RequireCycle(CommandLine commandLine, int studioLine, string AbortTas($"Cycle '{name}' has no corresponding WaitCycle"); return; } - if (cycleData.ContainsKey(name)) { + if (CycleData.ContainsKey(name)) { AbortTas($"Cycle '{name}' has already used with a RequireCycle"); return; } - cycleData[name] = (0.0f, 0.0f, 0, new float[controller.CurrentParsingFrame - waitCycles[name]]); + CycleData[name] = (0.0f, 0.0f, 0, new float[controller.CurrentParsingFrame - waitCycles[name]]); } if (!Enum.TryParse(commandLine.Arguments[1], out var type)) { @@ -169,7 +171,7 @@ private static void RequireCycle(CommandLine commandLine, int studioLine, string return; } - var data = cycleData[name]; + var data = CycleData[name]; // First guess the amount of frames we need to wait for the desired interval // Then validate and adjust to the correct value by applying the entire delta-time chain since the WaitCycle