Skip to content

Commit 41ff651

Browse files
committed
reimplement ForEachBreakPoints_EntityList, todo: make it compatible with other mod hooks
1 parent b19240e commit 41ff651

10 files changed

+1169
-1066
lines changed

Diff for: Docs/Commands.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- e.g. ooo_add_target CrystalStaticSpinner[c1:02]
77
- e.g. ooo_add_target DustStaticSpinner, which automatically adds all DustStaticSpinner in current room
88
- e.g. ooo_add_target Each, which automatically adds all entities in current room
9+
- e.g. ooo_add_target Auto, which makes the game automatically stop if current entity's update changes player's Position/Speed/State/...
910

1011
### ooo_remove_target
1112
- `ooo_remove_target entityID`
@@ -23,9 +24,9 @@
2324
- This command adds all PlayerCollider belonging to this entity as for-each breakpoints in PlayerCollider checks
2425
- e.g. ooo_add_target_pc Spring
2526
- e.g. ooo_add_target_pc Spikes[a1:09]
26-
- e.g. ooo_add_target_pc Auto
27-
- The grammar is almost same as ooo_add_target, but without the "except" grammar
28-
- Instead, we have "ooo_add_target_pc Auto", which makes the game automatically stop if a PlayerCollider collides with player
27+
- e.g. ooo_add_target_pc Each
28+
- e.g. ooo_add_target_pc Auto, which makes the game automatically stop if a PlayerCollider collides with player
29+
- The grammar is same as that of ooo_add_target
2930

3031
### ooo_remove_target_pc
3132
- `ooo_remove_target_pc entityID`

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ A Celeste Mod designed to be a tool in TAS making.
7979
- Celeste TAS hotkeys randomly work improperly -> Not sure if it's caused by TAS Helper.
8080

8181
- Actual Collide Hitboxes (of this frame) get lost after SL by predictor.
82+
83+
- Use SRT save, then reload asset, then SRT load. This causes crash -> I guess it's a general issue and only happens for mod developers, so just ignore it.

Diff for: Source/Entities/PauseUpdater.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Celeste.Mod.TASHelper.Entities;
55
public static class PauseUpdater {
66
// call entities updates when it's not called, e.g. by CelesteTAS pause, SkippingCutscene... which can not be set via Entity Tags like Tag.FrozenUpdater
77
internal static List<Entity> entities = new();
8-
private static readonly List<Entity> toRemove = new();
8+
private static List<Entity> toRemove = new();
99
private static bool updated = false;
1010
private static int levelPauseTags = Tags.FrozenUpdate | Tags.PauseUpdate | Tags.TransitionUpdate;
1111

@@ -15,6 +15,7 @@ public static void Load() {
1515
On.Celeste.Level.BeforeRender += OnBeforeRender;
1616
}
1717

18+
1819
[Unload]
1920

2021
public static void Unload() {

Diff for: Source/OrderOfOperation/BreakPoints.cs

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//#define OoO_Debug
2+
3+
using Mono.Cecil.Cil;
4+
using MonoMod.Cil;
5+
using MonoMod.RuntimeDetour;
6+
using System.Reflection;
7+
using static Celeste.Mod.TASHelper.OrderOfOperation.OoO_Core;
8+
9+
namespace Celeste.Mod.TASHelper.OrderOfOperation;
10+
11+
internal class BreakPoints {
12+
13+
// if a BreakPoint has subBreakPoints, make sure it's exactly before the method call, and emit Ret exactly after the method call
14+
// also, make sure if we jump to a breakpoint from start, the stack behavior is ok (i.e. no temp variable lives from before a breakpoint to after it)
15+
16+
public const string Prefix = "TAS Helper OoO_Core::";
17+
18+
public static readonly Dictionary<string, BreakPoints> dictionary = new();
19+
20+
public static readonly HashSet<string> HashPassedBreakPoints = new HashSet<string>();
21+
22+
public static readonly Dictionary<MethodBase, string> latestBreakpointBackup = new();
23+
24+
public static readonly List<string> passedBreakpoints = new();
25+
26+
public static readonly Dictionary<MethodBase, HashSet<BreakPoints>> detoursOnThisMethod = new();
27+
28+
#if OoO_Debug
29+
public static readonly HashSet<string> failedHooks = new();
30+
#endif
31+
32+
public int RetShift = 0;
33+
34+
public string UID;
35+
36+
public IDetour labelEmitter;
37+
38+
public MethodBase method;
39+
40+
public bool? SubMethodPassed = null;
41+
42+
internal const int RetShiftDoNotEmit = -100;
43+
44+
private BreakPoints(string ID, IDetour detour, MethodBase method) {
45+
UID = ID;
46+
labelEmitter = detour;
47+
this.method = method;
48+
}
49+
50+
public static BreakPoints Create(MethodBase method, string label, params Func<Instruction, bool>[] predicates) {
51+
return CreateFull(method, label, 0, NullAction, NullAction, predicates);
52+
}
53+
54+
public static BreakPoints CreateFull(MethodBase method, string label, int RetShift, Action<ILCursor> before, Action<ILCursor> after, params Func<Instruction, bool>[] predicates) {
55+
Func<string, Action<ILCursor, ILContext>> manipulator = (label) => (cursor, _) => {
56+
before(cursor);
57+
if (cursor.TryGotoNext(MoveType.AfterLabel, predicates)) {
58+
after(cursor);
59+
cursor.Emit(OpCodes.Ldstr, label);
60+
cursor.EmitDelegate(RecordLabel);
61+
if (RetShift > RetShiftDoNotEmit) {
62+
cursor.Index += RetShift; // when there's a method, which internally has breakpoints, exactly after this breakpoint, then we Ret after this method call
63+
cursor.Emit(OpCodes.Ret);
64+
}
65+
}
66+
#if OoO_Debug
67+
else {
68+
failedHooks.Add($"\n {label}");
69+
}
70+
#endif
71+
};
72+
return CreateImpl(method, label, manipulator, RetShift);
73+
}
74+
75+
internal static BreakPoints CreateImpl(MethodBase method, string label, Func<string, Action<ILCursor, ILContext>> manipulator, int RetShift = 0) {
76+
string ID = CreateUID(label);
77+
IDetour detour;
78+
using (new DetourContext { After = new List<string> { "*", "CelesteTAS-EverestInterop", "TASHelper", "TAS Helper OoO_Core Ending" }, ID = "TAS Helper OoO_Core BreakPoints" }) {
79+
detour = new ILHook(method, il => {
80+
ILCursor cursor = new(il);
81+
manipulator(ID)(cursor, il);
82+
}, manualConfig);
83+
}
84+
BreakPoints breakpoint = new BreakPoints(ID, detour, method);
85+
breakpoint.RetShift = RetShift;
86+
if (!detoursOnThisMethod.ContainsKey(method)) {
87+
detoursOnThisMethod[method] = new HashSet<BreakPoints>();
88+
}
89+
detoursOnThisMethod[method].Add(breakpoint);
90+
dictionary[ID] = breakpoint;
91+
return breakpoint;
92+
}
93+
94+
private static string CreateUID(string label) {
95+
string result = $"{Prefix}{label}";
96+
if (!dictionary.ContainsKey(result)) {
97+
return result;
98+
}
99+
int index = 1;
100+
do {
101+
result = $"{Prefix}{label}_{index}";
102+
index++;
103+
} while (dictionary.ContainsKey(result));
104+
return result;
105+
}
106+
internal static void RecordLabel(string label) {
107+
passedBreakpoints.Add(label);
108+
SendText(label); // if several labels are recorded in same frame, then the last one will be the output
109+
}
110+
111+
public BreakPoints AddAutoSkip() {
112+
AutoSkippedBreakpoints.Add(this.UID);
113+
return this;
114+
}
115+
116+
public BreakPoints RemoveAutoSkip() {
117+
AutoSkippedBreakpoints.Remove(this.UID);
118+
return this;
119+
}
120+
121+
public static BreakPoints MarkEnding(MethodBase method, string label, Action? afterRetAction = null, bool EmitRet = true, MoveType moveType = MoveType.AfterLabel) {
122+
string ID = CreateUID(label);
123+
IDetour detour;
124+
using (new DetourContext { After = new List<string> { "*", "CelesteTAS-EverestInterop", "TASHelper" }, ID = "TAS Helper OoO_Core Ending" }) {
125+
detour = new ILHook(method, il => {
126+
ILCursor cursor = new(il);
127+
while (cursor.TryGotoNext(moveType, i => i.OpCode == OpCodes.Ret)) {
128+
cursor.Emit(OpCodes.Ldstr, ID);
129+
cursor.EmitDelegate(RecordLabel);
130+
// don't know why but i fail to add a beforeRetAction here
131+
if (EmitRet) {
132+
cursor.Emit(OpCodes.Ret);
133+
}
134+
if (afterRetAction is not null) {
135+
cursor.EmitDelegate(afterRetAction);
136+
}
137+
cursor.Index++;
138+
}
139+
}, manualConfig);
140+
}
141+
BreakPoints breakpoint = new BreakPoints(ID, detour, method);
142+
if (!EmitRet) {
143+
breakpoint.RetShift = RetShiftDoNotEmit;
144+
}
145+
146+
if (!detoursOnThisMethod.ContainsKey(method)) {
147+
detoursOnThisMethod[method] = new HashSet<BreakPoints>();
148+
}
149+
detoursOnThisMethod[method].Add(breakpoint);
150+
dictionary[ID] = breakpoint;
151+
return breakpoint;
152+
}
153+
154+
public static void ReformHashPassedBreakPoints() {
155+
// we should not clear latestBreakpointBackup here
156+
foreach (string str in passedBreakpoints) {
157+
latestBreakpointBackup[dictionary[str].method] = str;
158+
}
159+
160+
HashPassedBreakPoints.Clear();
161+
foreach (string s in latestBreakpointBackup.Values) {
162+
HashPassedBreakPoints.Add(s);
163+
}
164+
165+
}
166+
167+
public static void ApplyAll() {
168+
foreach (BreakPoints breakPoints in dictionary.Values) {
169+
breakPoints.labelEmitter.Apply();
170+
}
171+
HashPassedBreakPoints.Clear();
172+
latestBreakpointBackup.Clear();
173+
passedBreakpoints.Clear();
174+
}
175+
176+
public static void UndoAll() {
177+
foreach (BreakPoints breakPoints in dictionary.Values) {
178+
breakPoints.labelEmitter.Undo();
179+
}
180+
HashPassedBreakPoints.Clear();
181+
latestBreakpointBackup.Clear();
182+
passedBreakpoints.Clear();
183+
}
184+
185+
[Unload]
186+
private static void Unload() {
187+
foreach (BreakPoints breakPoints in dictionary.Values) {
188+
breakPoints.labelEmitter.Dispose();
189+
}
190+
}
191+
}
192+
193+

0 commit comments

Comments
 (0)