Skip to content

Commit

Permalink
feat: remake the SaveAndQuitReenter command so that it always works i…
Browse files Browse the repository at this point in the history
…n input mode
  • Loading branch information
DemoJameson committed Oct 4, 2023
1 parent d12c131 commit 2008744
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 361 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace TAS.Input.Commands;

public static class SafeCommand {
// stop tas when out of Level/LevelLoader/LevelExit/Pico8/LevelEnter/LevelReenter(from CelesteTAS)
// stop tas when out of Level/LevelLoader/LevelExit/Pico8/LevelEnter
// stop tas when entering Options/ModOptions UI
public static bool DisallowUnsafeInput { get; set; } = true;
public static bool DisallowUnsafeInputParsing { get; private set; } = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,23 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Celeste;
using Mono.Cecil.Cil;
using Monocle;
using MonoMod.Utils;
using TAS.Module;
using TAS.Utils;

namespace TAS.Input.Commands;
namespace TAS.Input.Commands;

public static class SaveAndQuitReenterCommand {
public enum SaveAndQuitReenterMode {
Input,
Simulate
}

public class LevelReenter : Scene {
public LevelReenter(Session session) {
AreaData.Get(session).RestoreASideAreaData();
}

public override void Begin() {
base.Begin();

Entity routine = new() {new Coroutine(Routine())};
Add(routine);
Add(new HudRenderer());
}

private IEnumerator Routine() {
UserIO.SaveHandler(file: true, settings: true);
while (UserIO.Saving) yield return null;
while (SaveLoadIcon.OnScreen) yield return null;

int slot = SaveData.Instance.FileSlot;
var saveData = UserIO.Load<SaveData>(SaveData.GetFilename(slot));
SaveData.Start(saveData, slot);

LevelEnter.Go(SaveData.Instance.CurrentSession, fromSaveData: true);
}
}

private static bool justPressedSnQ = false;
public static SaveAndQuitReenterMode? LocalMode;
public static SaveAndQuitReenterMode? GlobalModeParsing;
public static SaveAndQuitReenterMode? GlobalModeRuntime;

private static SaveAndQuitReenterMode Mode {
get {
if (LibTasHelper.Exporting) {
return SaveAndQuitReenterMode.Input;
}

if (EnforceLegalCommand.EnabledWhenParsing) {
return SaveAndQuitReenterMode.Input;
}

SaveAndQuitReenterMode? globalMode = ParsingCommand ? GlobalModeParsing : GlobalModeRuntime;
return LocalMode ?? globalMode ?? SaveAndQuitReenterMode.Simulate;
}
}
private static bool justPressedSnQ;

private static int ActiveFileSlot {
get {
if (LibTasHelper.Exporting) {
return 0;
}

if (Engine.Scene is Overworld {Current: OuiFileSelect select}) {
return select.SlotIndex;
}
Expand All @@ -77,137 +27,94 @@ private static int ActiveFileSlot {
}

private static bool preventClear = false;

// Contains which slot was used for each command, to ensure that inputs before the current frame stay the same
public static Dictionary<int, int> InsertedSlots = new();

[Load]
private static void Load() {
typeof(Level)
.GetNestedType("<>c__DisplayClass149_0", BindingFlags.NonPublic)
.GetMethod("<Pause>b__8", BindingFlags.NonPublic | BindingFlags.Instance)
.IlHook((cursor, _) => cursor.Emit(OpCodes.Ldc_I4_1)
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));

typeof(Level).GetMethod("Update").IlHook((cursor, _) => cursor.Emit(OpCodes.Ldc_I4_0)
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));
}

[ClearInputs]
private static void Clear() {
if (preventClear) return;
InsertedSlots.Clear();
}

[ClearInputs]
[ParseFileEnd]
private static void ParseFileEnd() {
GlobalModeParsing = null;
}

[DisableRun]
private static void DisableRun() {
LocalMode = null;
GlobalModeRuntime = null;
justPressedSnQ = false;
}

[TasCommand("SaveAndQuitReenter", ExecuteTiming = ExecuteTiming.Parse | ExecuteTiming.Runtime)]
private static void SaveAndQuitReenter(string[] args, int studioLine, string filePath, int fileLine) {
LocalMode = null;
InputController controller = Manager.Controller;

if (args.IsNotEmpty()) {
if (Enum.TryParse(args[0], true, out SaveAndQuitReenterMode value)) {
LocalMode = value;
} else if (ParsingCommand) {
AbortTas("SaveAndQuitReenter command failed.\nMode must be Input or Simulate");
return;
if (ParsingCommand) {
int slot = ActiveFileSlot;
if (InsertedSlots.TryGetValue(studioLine, out int prevSlot)) {
slot = prevSlot;
} else {
InsertedSlots[studioLine] = slot;
}
}

if (ParsingCommand) {
if (Mode == SaveAndQuitReenterMode.Simulate) {
// Wait for the Save & Quit wipe
Manager.Controller.AddFrames("32", studioLine);
bool isSafe = !SafeCommand.DisallowUnsafeInputParsing;

LibTasHelper.AddInputFrame("58");
controller.AddFrames("31", studioLine);
Command.TryParse(controller, filePath, fileLine, "Unsafe", controller.CurrentParsingFrame, studioLine, out _);
controller.AddFrames("14", studioLine);
if (slot == -1) {
// Load debug slot
controller.AddFrames("1,D", studioLine);
controller.AddFrames("1,O", studioLine);
controller.AddFrames("33", studioLine);
} else {
if (SafeCommand.DisallowUnsafeInputParsing) {
AbortTas("\"SaveAndQuitReenter, Input\" requires unsafe inputs");
return;
}

int slot = ActiveFileSlot;
if (InsertedSlots.TryGetValue(studioLine, out int prevSlot)) {
slot = prevSlot;
// Get to the save files screen
controller.AddFrames("1,O", studioLine);
controller.AddFrames("56", studioLine);
// Alternate 1,D and 1,F,180 to select the slot
for (int i = 0; i < slot; i++) {
controller.AddFrames(i % 2 == 0 ? "1,D" : "1,F,180", studioLine);
}

LibTasHelper.AddInputFrame("58");
Manager.Controller.AddFrames("31", studioLine);
Manager.Controller.AddFrames("14", studioLine);

if (slot == -1) {
// Load debug slot
Manager.Controller.AddFrames("1,D", studioLine);
Manager.Controller.AddFrames("1,O", studioLine);
Manager.Controller.AddFrames("33", studioLine);
} else {
// Get to the save files screen
Manager.Controller.AddFrames("1,O", studioLine);
Manager.Controller.AddFrames("56", studioLine);
// Alternate 1,D and 1,F,180 to select the slot
for (int i = 0; i < slot; i++) {
Manager.Controller.AddFrames(i % 2 == 0 ? "1,D" : "1,F,180", studioLine);
}
// Load the selected save file
Manager.Controller.AddFrames("1,O", studioLine);
Manager.Controller.AddFrames("14", studioLine);
Manager.Controller.AddFrames("1,O", studioLine);
Manager.Controller.AddFrames("1", studioLine);
LibTasHelper.AddInputFrame("32");
}
// Load the selected save file
controller.AddFrames("1,O", studioLine);
controller.AddFrames("14", studioLine);
controller.AddFrames("1,O", studioLine);
controller.AddFrames("1", studioLine);
LibTasHelper.AddInputFrame("32");
}

InsertedSlots[studioLine] = slot;
Command.TryParse(controller, filePath, fileLine, isSafe ? "Safe" : "Unsafe", controller.CurrentParsingFrame, studioLine, out _);
} else {
if (!justPressedSnQ) {
AbortTas("SaveAndQuitReenter must be exactly after pressing the \"Save & Quit\" button");
return;
}

return;
}

if (!justPressedSnQ) {
AbortTas("SaveAndQuitReenter must be exactly after pressing the \"Save & Quit\" button");
return;
}

if (Engine.Scene is not Level level) {
AbortTas("SaveAndQuitReenter can't be used outside levels");
return;
}
if (Engine.Scene is not Level level) {
AbortTas("SaveAndQuitReenter can't be used outside levels");
return;
}

if (Mode == SaveAndQuitReenterMode.Simulate) {
// Replace the Save & Quit wipe with our work action
level.Wipe.OnComplete = delegate {
Engine.Scene = new LevelReenter(level.Session);
};
} else {
// Re-insert inputs of the save file slot changed
if (InsertedSlots.TryGetValue(studioLine, out int slot) && slot != ActiveFileSlot) {
InsertedSlots[studioLine] = ActiveFileSlot;
// Avoid clearing our InsertedSlots info
preventClear = true;
Manager.Controller.NeedsReload = true;
Manager.Controller.RefreshInputs(enableRun: false);
preventClear = false;
}
}
}

[TasCommand("SaveAndQuitReenterMode", ExecuteTiming = ExecuteTiming.Parse | ExecuteTiming.Runtime)]
private static void StunPauseCommandMode(string[] args) {
if (args.IsNotEmpty() && Enum.TryParse(args[0], true, out SaveAndQuitReenterMode value)) {
if (ParsingCommand) {
GlobalModeParsing = value;
} else {
GlobalModeRuntime = value;
// Avoid clearing our InsertedSlots info when RefreshInputs()
Dictionary<int, int> backup = new(InsertedSlots);
controller.NeedsReload = true;
controller.RefreshInputs(enableRun: false);
InsertedSlots.Clear();
InsertedSlots.AddRange(backup);
}
} else if (ParsingCommand) {
AbortTas("SaveAndQuitReenterMode command failed.\nMode must be Input or Simulate");
}
}
}
4 changes: 2 additions & 2 deletions CelesteTAS-EverestInterop/Source/TAS/Manager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static void Update() {
if (!canPlayback) {
DisableRun();
} else if (SafeCommand.DisallowUnsafeInput && Controller.CurrentFrameInTas > 1) {
if (Engine.Scene is not (Level or LevelLoader or LevelExit or Emulator or LevelEnter or SaveAndQuitReenterCommand.LevelReenter)) {
if (Engine.Scene is not (Level or LevelLoader or LevelExit or Emulator or LevelEnter)) {
DisableRun();
} else if (Engine.Scene is Level level && level.Tracker.GetEntity<TextMenu>() is { } menu) {
if (menu.Items.FirstOrDefault() is TextMenu.Header header && header.Title == Dialog.Clean("options_title") ||
Expand Down Expand Up @@ -248,7 +248,7 @@ overworld.Next is OuiChapterSelect && UserIO.Saving ||
case Emulator emulator:
return emulator.game == null;
default:
bool isLoading = Engine.Scene is LevelExit or LevelLoader or GameLoader or SaveAndQuitReenterCommand.LevelReenter || Engine.Scene.GetType().Name == "LevelExitToLobby";
bool isLoading = Engine.Scene is LevelExit or LevelLoader or GameLoader || Engine.Scene.GetType().Name == "LevelExitToLobby";
return isLoading;
}
}
Expand Down
6 changes: 0 additions & 6 deletions CelesteTAS-EverestInterop/Source/Utils/SpeedrunToolUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ internal static class SpeedrunToolUtils {
private static long? tasStartFileTime;
private static MouseState mouseState;
private static Dictionary<Follower, bool> followers;
private static SaveAndQuitReenterCommand.SaveAndQuitReenterMode? localSnQMode;
private static SaveAndQuitReenterCommand.SaveAndQuitReenterMode? globalSnQMode;
private static Dictionary<int, int> insertedSlots = new();
private static bool disallowUnsafeInput;

Expand All @@ -48,8 +46,6 @@ public static void AddSaveLoadAction() {
tasStartFileTime = MetadataCommands.TasStartFileTime;
mouseState = MouseCommand.CurrentState;
followers = HitboxSimplified.Followers.DeepCloneShared();
localSnQMode = SaveAndQuitReenterCommand.LocalMode;
globalSnQMode = SaveAndQuitReenterCommand.GlobalModeRuntime;
insertedSlots = SaveAndQuitReenterCommand.InsertedSlots.DeepCloneShared();
disallowUnsafeInput = SafeCommand.DisallowUnsafeInput;
};
Expand All @@ -71,8 +67,6 @@ public static void AddSaveLoadAction() {
MetadataCommands.TasStartFileTime = tasStartFileTime;
MouseCommand.CurrentState = mouseState;
HitboxSimplified.Followers = followers.DeepCloneShared();
SaveAndQuitReenterCommand.LocalMode = localSnQMode;
SaveAndQuitReenterCommand.GlobalModeRuntime = globalSnQMode;
SaveAndQuitReenterCommand.InsertedSlots = insertedSlots.DeepCloneShared();
SafeCommand.DisallowUnsafeInput = disallowUnsafeInput;
};
Expand Down
17 changes: 3 additions & 14 deletions Docs/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,10 @@ Specify the default mode for `StunPause` command.

### SaveAndQuitReenter
- ```
SaveAndQuitReenter, (optional mode, Simulate or Input)
SaveAndQuitReenter
```
- It is important to place the command directly after pressing the `Save & Quit` button
- Simulate mode should be used for working on individual levels, since it doesn't require `Unsafe`.
- Input mode should be used for a full-game run, by using `SaveAndQuitReenterMode`.
- Inserts the inputs required to Save & Quit and to reenter the correct save file.
- It is important to place the command directly after pressing the `Save & Quit` button.

e.g.
```
Expand All @@ -241,16 +240,6 @@ Specify the default mode for `StunPause` command.
#Respawn animation
36
```

- The command's mode is determined by several things, the priority from high to low are:
1. [EnforceLegal](#enforcelegal) command force all the commands use `Input` mode
2. Mode specified by the `SaveAndQuitReenter` command
3. Mode specified by the `SaveAndQuitReenterMode` command
4. Default mode is `Simulate`

### SaveAndQuitReenterMode
Specify the default mode for `SaveAndQuitReenter` command.
- `SaveAndQuitReenterMode, Simulate/Input`

### StartRecording and StopRecording
NOTE: These commands require [TAS Recorder](https://gamebanana.com/tools/14085)!
Expand Down
Loading

0 comments on commit 2008744

Please sign in to comment.