Skip to content

Commit f7f82bb

Browse files
committed
feat: remake the SaveAndQuitReenter command so that it always works in input mode
1 parent d12c131 commit f7f82bb

File tree

7 files changed

+243
-361
lines changed

7 files changed

+243
-361
lines changed

CelesteTAS-EverestInterop/Source/TAS/Input/Commands/SafeCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace TAS.Input.Commands;
22

33
public static class SafeCommand {
4-
// stop tas when out of Level/LevelLoader/LevelExit/Pico8/LevelEnter/LevelReenter(from CelesteTAS)
4+
// stop tas when out of Level/LevelLoader/LevelExit/Pico8/LevelEnter
55
// stop tas when entering Options/ModOptions UI
66
public static bool DisallowUnsafeInput { get; set; } = true;
77
public static bool DisallowUnsafeInputParsing { get; private set; } = true;
Lines changed: 55 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,23 @@
1-
using System;
2-
using System.Collections;
31
using System.Collections.Generic;
42
using System.Reflection;
53
using Celeste;
64
using Mono.Cecil.Cil;
75
using Monocle;
6+
using MonoMod.Utils;
87
using TAS.Module;
98
using TAS.Utils;
109

11-
namespace TAS.Input.Commands;
10+
namespace TAS.Input.Commands;
1211

1312
public static class SaveAndQuitReenterCommand {
14-
public enum SaveAndQuitReenterMode {
15-
Input,
16-
Simulate
17-
}
18-
19-
public class LevelReenter : Scene {
20-
public LevelReenter(Session session) {
21-
AreaData.Get(session).RestoreASideAreaData();
22-
}
23-
24-
public override void Begin() {
25-
base.Begin();
26-
27-
Entity routine = new() {new Coroutine(Routine())};
28-
Add(routine);
29-
Add(new HudRenderer());
30-
}
31-
32-
private IEnumerator Routine() {
33-
UserIO.SaveHandler(file: true, settings: true);
34-
while (UserIO.Saving) yield return null;
35-
while (SaveLoadIcon.OnScreen) yield return null;
36-
37-
int slot = SaveData.Instance.FileSlot;
38-
var saveData = UserIO.Load<SaveData>(SaveData.GetFilename(slot));
39-
SaveData.Start(saveData, slot);
40-
41-
LevelEnter.Go(SaveData.Instance.CurrentSession, fromSaveData: true);
42-
}
43-
}
44-
45-
private static bool justPressedSnQ = false;
46-
public static SaveAndQuitReenterMode? LocalMode;
47-
public static SaveAndQuitReenterMode? GlobalModeParsing;
48-
public static SaveAndQuitReenterMode? GlobalModeRuntime;
49-
50-
private static SaveAndQuitReenterMode Mode {
51-
get {
52-
if (LibTasHelper.Exporting) {
53-
return SaveAndQuitReenterMode.Input;
54-
}
55-
56-
if (EnforceLegalCommand.EnabledWhenParsing) {
57-
return SaveAndQuitReenterMode.Input;
58-
}
59-
60-
SaveAndQuitReenterMode? globalMode = ParsingCommand ? GlobalModeParsing : GlobalModeRuntime;
61-
return LocalMode ?? globalMode ?? SaveAndQuitReenterMode.Simulate;
62-
}
63-
}
13+
private static bool justPressedSnQ;
6414

6515
private static int ActiveFileSlot {
6616
get {
6717
if (LibTasHelper.Exporting) {
6818
return 0;
6919
}
70-
20+
7121
if (Engine.Scene is Overworld {Current: OuiFileSelect select}) {
7222
return select.SlotIndex;
7323
}
@@ -77,137 +27,94 @@ private static int ActiveFileSlot {
7727
}
7828

7929
private static bool preventClear = false;
30+
8031
// Contains which slot was used for each command, to ensure that inputs before the current frame stay the same
8132
public static Dictionary<int, int> InsertedSlots = new();
82-
33+
8334
[Load]
8435
private static void Load() {
8536
typeof(Level)
8637
.GetNestedType("<>c__DisplayClass149_0", BindingFlags.NonPublic)
8738
.GetMethod("<Pause>b__8", BindingFlags.NonPublic | BindingFlags.Instance)
8839
.IlHook((cursor, _) => cursor.Emit(OpCodes.Ldc_I4_1)
8940
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));
90-
41+
9142
typeof(Level).GetMethod("Update").IlHook((cursor, _) => cursor.Emit(OpCodes.Ldc_I4_0)
92-
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));
43+
.Emit(OpCodes.Stsfld, typeof(SaveAndQuitReenterCommand).GetFieldInfo(nameof(justPressedSnQ))));
9344
}
9445

9546
[ClearInputs]
9647
private static void Clear() {
97-
if (preventClear) return;
9848
InsertedSlots.Clear();
9949
}
10050

101-
[ClearInputs]
102-
[ParseFileEnd]
103-
private static void ParseFileEnd() {
104-
GlobalModeParsing = null;
105-
}
106-
10751
[DisableRun]
10852
private static void DisableRun() {
109-
LocalMode = null;
110-
GlobalModeRuntime = null;
11153
justPressedSnQ = false;
11254
}
113-
55+
11456
[TasCommand("SaveAndQuitReenter", ExecuteTiming = ExecuteTiming.Parse | ExecuteTiming.Runtime)]
11557
private static void SaveAndQuitReenter(string[] args, int studioLine, string filePath, int fileLine) {
116-
LocalMode = null;
58+
InputController controller = Manager.Controller;
11759

118-
if (args.IsNotEmpty()) {
119-
if (Enum.TryParse(args[0], true, out SaveAndQuitReenterMode value)) {
120-
LocalMode = value;
121-
} else if (ParsingCommand) {
122-
AbortTas("SaveAndQuitReenter command failed.\nMode must be Input or Simulate");
123-
return;
60+
if (ParsingCommand) {
61+
int slot = ActiveFileSlot;
62+
if (InsertedSlots.TryGetValue(studioLine, out int prevSlot)) {
63+
slot = prevSlot;
64+
} else {
65+
InsertedSlots[studioLine] = slot;
12466
}
125-
}
12667

127-
if (ParsingCommand) {
128-
if (Mode == SaveAndQuitReenterMode.Simulate) {
129-
// Wait for the Save & Quit wipe
130-
Manager.Controller.AddFrames("32", studioLine);
68+
bool isSafe = SafeCommand.DisallowUnsafeInputParsing;
69+
70+
LibTasHelper.AddInputFrame("58");
71+
controller.AddFrames("31", studioLine);
72+
Command.TryParse(controller, filePath, fileLine, "Unsafe", controller.CurrentParsingFrame, studioLine, out _);
73+
controller.AddFrames("14", studioLine);
74+
if (slot == -1) {
75+
// Load debug slot
76+
controller.AddFrames("1,D", studioLine);
77+
controller.AddFrames("1,O", studioLine);
78+
controller.AddFrames("33", studioLine);
13179
} else {
132-
if (SafeCommand.DisallowUnsafeInputParsing) {
133-
AbortTas("\"SaveAndQuitReenter, Input\" requires unsafe inputs");
134-
return;
135-
}
136-
137-
int slot = ActiveFileSlot;
138-
if (InsertedSlots.TryGetValue(studioLine, out int prevSlot)) {
139-
slot = prevSlot;
80+
// Get to the save files screen
81+
controller.AddFrames("1,O", studioLine);
82+
controller.AddFrames("56", studioLine);
83+
// Alternate 1,D and 1,F,180 to select the slot
84+
for (int i = 0; i < slot; i++) {
85+
controller.AddFrames(i % 2 == 0 ? "1,D" : "1,F,180", studioLine);
14086
}
14187

142-
LibTasHelper.AddInputFrame("58");
143-
Manager.Controller.AddFrames("31", studioLine);
144-
Manager.Controller.AddFrames("14", studioLine);
145-
146-
if (slot == -1) {
147-
// Load debug slot
148-
Manager.Controller.AddFrames("1,D", studioLine);
149-
Manager.Controller.AddFrames("1,O", studioLine);
150-
Manager.Controller.AddFrames("33", studioLine);
151-
} else {
152-
// Get to the save files screen
153-
Manager.Controller.AddFrames("1,O", studioLine);
154-
Manager.Controller.AddFrames("56", studioLine);
155-
// Alternate 1,D and 1,F,180 to select the slot
156-
for (int i = 0; i < slot; i++) {
157-
Manager.Controller.AddFrames(i % 2 == 0 ? "1,D" : "1,F,180", studioLine);
158-
}
159-
// Load the selected save file
160-
Manager.Controller.AddFrames("1,O", studioLine);
161-
Manager.Controller.AddFrames("14", studioLine);
162-
Manager.Controller.AddFrames("1,O", studioLine);
163-
Manager.Controller.AddFrames("1", studioLine);
164-
LibTasHelper.AddInputFrame("32");
165-
}
88+
// Load the selected save file
89+
controller.AddFrames("1,O", studioLine);
90+
controller.AddFrames("14", studioLine);
91+
controller.AddFrames("1,O", studioLine);
92+
controller.AddFrames("1", studioLine);
93+
LibTasHelper.AddInputFrame("32");
94+
}
16695

167-
InsertedSlots[studioLine] = slot;
96+
Command.TryParse(controller, filePath, fileLine, isSafe ? "Safe" : "Unsafe", controller.CurrentParsingFrame, studioLine, out _);
97+
} else {
98+
if (!justPressedSnQ) {
99+
AbortTas("SaveAndQuitReenter must be exactly after pressing the \"Save & Quit\" button");
100+
return;
168101
}
169-
170-
return;
171-
}
172102

173-
if (!justPressedSnQ) {
174-
AbortTas("SaveAndQuitReenter must be exactly after pressing the \"Save & Quit\" button");
175-
return;
176-
}
177-
178-
if (Engine.Scene is not Level level) {
179-
AbortTas("SaveAndQuitReenter can't be used outside levels");
180-
return;
181-
}
103+
if (Engine.Scene is not Level level) {
104+
AbortTas("SaveAndQuitReenter can't be used outside levels");
105+
return;
106+
}
182107

183-
if (Mode == SaveAndQuitReenterMode.Simulate) {
184-
// Replace the Save & Quit wipe with our work action
185-
level.Wipe.OnComplete = delegate {
186-
Engine.Scene = new LevelReenter(level.Session);
187-
};
188-
} else {
189108
// Re-insert inputs of the save file slot changed
190109
if (InsertedSlots.TryGetValue(studioLine, out int slot) && slot != ActiveFileSlot) {
191110
InsertedSlots[studioLine] = ActiveFileSlot;
192-
// Avoid clearing our InsertedSlots info
193-
preventClear = true;
194-
Manager.Controller.NeedsReload = true;
195-
Manager.Controller.RefreshInputs(enableRun: false);
196-
preventClear = false;
197-
}
198-
}
199-
}
200-
201-
[TasCommand("SaveAndQuitReenterMode", ExecuteTiming = ExecuteTiming.Parse | ExecuteTiming.Runtime)]
202-
private static void StunPauseCommandMode(string[] args) {
203-
if (args.IsNotEmpty() && Enum.TryParse(args[0], true, out SaveAndQuitReenterMode value)) {
204-
if (ParsingCommand) {
205-
GlobalModeParsing = value;
206-
} else {
207-
GlobalModeRuntime = value;
111+
// Avoid clearing our InsertedSlots info when RefreshInputs()
112+
Dictionary<int, int> backup = new(InsertedSlots);
113+
controller.NeedsReload = true;
114+
controller.RefreshInputs(enableRun: false);
115+
InsertedSlots.Clear();
116+
InsertedSlots.AddRange(backup);
208117
}
209-
} else if (ParsingCommand) {
210-
AbortTas("SaveAndQuitReenterMode command failed.\nMode must be Input or Simulate");
211118
}
212119
}
213120
}

CelesteTAS-EverestInterop/Source/TAS/Manager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static void Update() {
8080
if (!canPlayback) {
8181
DisableRun();
8282
} else if (SafeCommand.DisallowUnsafeInput && Controller.CurrentFrameInTas > 1) {
83-
if (Engine.Scene is not (Level or LevelLoader or LevelExit or Emulator or LevelEnter or SaveAndQuitReenterCommand.LevelReenter)) {
83+
if (Engine.Scene is not (Level or LevelLoader or LevelExit or Emulator or LevelEnter)) {
8484
DisableRun();
8585
} else if (Engine.Scene is Level level && level.Tracker.GetEntity<TextMenu>() is { } menu) {
8686
if (menu.Items.FirstOrDefault() is TextMenu.Header header && header.Title == Dialog.Clean("options_title") ||
@@ -248,7 +248,7 @@ overworld.Next is OuiChapterSelect && UserIO.Saving ||
248248
case Emulator emulator:
249249
return emulator.game == null;
250250
default:
251-
bool isLoading = Engine.Scene is LevelExit or LevelLoader or GameLoader or SaveAndQuitReenterCommand.LevelReenter || Engine.Scene.GetType().Name == "LevelExitToLobby";
251+
bool isLoading = Engine.Scene is LevelExit or LevelLoader or GameLoader || Engine.Scene.GetType().Name == "LevelExitToLobby";
252252
return isLoading;
253253
}
254254
}

CelesteTAS-EverestInterop/Source/Utils/SpeedrunToolUtils.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ internal static class SpeedrunToolUtils {
2828
private static long? tasStartFileTime;
2929
private static MouseState mouseState;
3030
private static Dictionary<Follower, bool> followers;
31-
private static SaveAndQuitReenterCommand.SaveAndQuitReenterMode? localSnQMode;
32-
private static SaveAndQuitReenterCommand.SaveAndQuitReenterMode? globalSnQMode;
3331
private static Dictionary<int, int> insertedSlots = new();
3432
private static bool disallowUnsafeInput;
3533

@@ -48,8 +46,6 @@ public static void AddSaveLoadAction() {
4846
tasStartFileTime = MetadataCommands.TasStartFileTime;
4947
mouseState = MouseCommand.CurrentState;
5048
followers = HitboxSimplified.Followers.DeepCloneShared();
51-
localSnQMode = SaveAndQuitReenterCommand.LocalMode;
52-
globalSnQMode = SaveAndQuitReenterCommand.GlobalModeRuntime;
5349
insertedSlots = SaveAndQuitReenterCommand.InsertedSlots.DeepCloneShared();
5450
disallowUnsafeInput = SafeCommand.DisallowUnsafeInput;
5551
};
@@ -71,8 +67,6 @@ public static void AddSaveLoadAction() {
7167
MetadataCommands.TasStartFileTime = tasStartFileTime;
7268
MouseCommand.CurrentState = mouseState;
7369
HitboxSimplified.Followers = followers.DeepCloneShared();
74-
SaveAndQuitReenterCommand.LocalMode = localSnQMode;
75-
SaveAndQuitReenterCommand.GlobalModeRuntime = globalSnQMode;
7670
SaveAndQuitReenterCommand.InsertedSlots = insertedSlots.DeepCloneShared();
7771
SafeCommand.DisallowUnsafeInput = disallowUnsafeInput;
7872
};

Docs/Commands.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,10 @@ Specify the default mode for `StunPause` command.
224224

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

233232
e.g.
234233
```
@@ -241,16 +240,6 @@ Specify the default mode for `StunPause` command.
241240
#Respawn animation
242241
36
243242
```
244-
245-
- The command's mode is determined by several things, the priority from high to low are:
246-
1. [EnforceLegal](#enforcelegal) command force all the commands use `Input` mode
247-
2. Mode specified by the `SaveAndQuitReenter` command
248-
3. Mode specified by the `SaveAndQuitReenterMode` command
249-
4. Default mode is `Simulate`
250-
251-
### SaveAndQuitReenterMode
252-
Specify the default mode for `SaveAndQuitReenter` command.
253-
- `SaveAndQuitReenterMode, Simulate/Input`
254243

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

0 commit comments

Comments
 (0)