-
Notifications
You must be signed in to change notification settings - Fork 3
/
escape_backrooms.asl
339 lines (291 loc) · 12.8 KB
/
escape_backrooms.asl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/*
==1.21 - 4.0+ Autosplitter==
Reworked by theframeburglar
==4.0+ Autosplitter==
Variables for 4.0+ found by Reokin, thanks to theframeburglar for teaching me how to do it!
Restructured init
==3.13 (Christmas) Autosplitter==
==3.11 (Halloween) Autosplitter==
==3.10 Autosplitter==
Authored by Permamiss & HeXaGoN
3.0 - 3.9 variables updated to work with newer versions by theframeburglar
==3.0 - 3.9 Autosplitter==
Authored by Permamiss & HeXaGoN
isLoading1, wasLoading1 variables found by HeXaGoN
isLoading2, wasLoading2, multiplayer variables found by Permamiss
Shoutouts to Xero for consulting, and to Shad0w & for being our Fancy messenger!
==2.3/2.9 Autosplitter==
Authored by Xero
This should be considered a legacy product. Use at your own risk.
==Documentation Notes==
Better and more general documentation for LiveSplit autosplitters can be found at https://github.com/LiveSplit/LiveSplit.AutoSplitters/blob/master/README.md
Timer code can be found at https://github.com/LiveSplit/LiveSplit/blob/master/LiveSplit/LiveSplit.Core/Model/TimerModel.cs
"vars" is a persistent object that is able to contain persistent variables
"old" contains the values of all the defined variables in the last update
"current" contains the current values of all the defined variables
"settings" is an object used to add or get settings
*/
state("Backrooms-Win64-Shipping", "1.21 - 4.0+")
{
}
startup
{
settings.Add("disable_restart_time_removal", false, "Disable pausing of autosplitter when restarting levels");
}
init
{
game.Suspend();
// Latent action UUIDs to check
// Keep last one as 0 so we know when to stop
// 1.0 - Mostly the same for 2.0 and 3.0
// sp 00000000D4D1BDD5
// mp 000000008FD0BC64
// end game sp 0x0000000085969B25
// 3.0
// end game sp 000000004CBFBB29
// 4.0
// sp 000000002F9930F6
// mp 000000008E2A5E5E
// end game sp 0000000080D6D177
var UUIDs = new ulong[] {0x0000000085969B25, 0x00000000D4D1BDD5, 0x000000008FD0BC64, 0x000000004CBFBB29, 0x000000002F9930F6, 0x000000008E2A5E5E, 0x0000000080D6D177, 0};
vars.arrUUIDs = game.AllocateMemory(8 * UUIDs.Length);
for (int i = 0; i < UUIDs.Length; i++)
{
memory.WriteValue<ulong>((IntPtr)vars.arrUUIDs + (i * 8), UUIDs[i]);
}
vars.watchers = new MemoryWatcherList();
var scanner = new SignatureScanner(game, game.MainModule.BaseAddress, modules.First().ModuleMemorySize);
// RestartGame hook
IntPtr ptrIsRestartingAddr = scanner.Scan(new SigScanTarget(0, // target the 0th byte
"48 8B F9", // mov rdi, rcx
"48 8B 89 78 02 00 00" // mov rcx, qword ptr ds:[rcx+0x278]
));
if (ptrIsRestartingAddr == IntPtr.Zero)
{
throw new Exception("Could not find isRestarting detour!");
}
// If the scan was successful then we probably have a good version
version = "1.21 - 4.0+";
// isRestarting == 1 means restarting
// isRestarting == 0 means not restarting
vars.isRestarting = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for variable
memory.WriteValue<byte>((IntPtr)vars.isRestarting, 0); // Set isRestarting to 0 which is the default "not restarting" state
vars.isRestartingDetour = vars.isRestarting + 2; // skip over the first two bytes
vars.watchers.Add(new MemoryWatcher<bool>(new DeepPointer((IntPtr)vars.isRestarting)){ Name = "isRestarting" });
var isRestartingDetourBytes = new byte[]
{
0x66, 0xC7, 0x05, 0xF5, 0xFF, 0xFF, 0xFF, 0x01, 0x00, // mov word ptr ds:[7FF78D6366BD],1 (set isRestarting to 1)
// original instructions
0x48, 0x8B, 0xF9, // mov rdi, rcx
0x48, 0x8B, 0x89, 0x78, 0x02, 0x00, 0x00, // mov rcx, qword ptr ds:[rcx+0x278]
0x48, 0x8b, 0x01, //mov rax, qword ptr ds:[rcx]
0xC3 // ret
};
// bytes to detour load start function
var isRestartingHookBytes = new List<byte>()
{
0x48, 0xBF // mov rdi, jumploc
};
isRestartingHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.isRestartingDetour));
isRestartingHookBytes.AddRange(new byte[] {
0xFF, 0xD7, // call rdi
0x90
});
print("[EtB Autosplitter] DEBUG: Escape the Backrooms Autosplitter loaded");
print("[EtB Autosplitter] DEBUG: Detected game version: " + (version == "" ? "Unknown version - heap size " + modules.First().ModuleMemorySize.ToString() + ", please contact the autosplitter authors for help!" : version));
// Load level hook -- isLoadingLevel which tells us if the game is loading a level and which map we're coming from
IntPtr ptrisLoadingLevelAddr = scanner.Scan(new SigScanTarget(0, // target the 0th bytes
"45 33 C9", // xor r9d, r9d
"48 8B D7", // mov rdx, rdi
"49 8B CC", // mov rcx, r12
"FF 93 70 04 00 00", // call qword ptr ds:[rbx+470] - assume 470 for all versions but it could change
"84 C0", // test al, al
"49 8B CE", // mov rcx, r14
"40 0F 94 C6" // sete sil
));
if (ptrisLoadingLevelAddr == IntPtr.Zero)
{
throw new Exception("Could not find isLoadingLevel detour!");
}
// isLoadingLevel == 1 means loading (before fade to black)
// isLoadingLevel == 0 means not loading (after spinning loading screen)
vars.isLoadingLevel = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for my variable
memory.WriteValue<byte>((IntPtr)vars.isLoadingLevel, 0); // Set isLoadingLevel to 0 which is the default "not loading" state
memory.WriteValue<ulong>((IntPtr)vars.isLoadingLevel + 2, 0x006E00690061004D); // first 4 letters of "MainMenuMap"
memory.WriteValue<ulong>((IntPtr)vars.isLoadingLevel + 10, 0x00620062006f004c); // first 4 letters of "Lobby"
vars.isLoadingLevelDetour = vars.isLoadingLevel + 2 + 16; // skip over the first two bytes plus 16 to account for our map strings
vars.watchers.Add(new MemoryWatcher<bool>(new DeepPointer((IntPtr)vars.isLoadingLevel)){ Name = "isLoadingLevel" });
var isLoadingLevelDetourBytes = new byte[]
{
// Check to see what map we are loading
0x50, // push rax
0x53, // push rbx
0x49, 0x8B, 0x46, 0x28, // mov rax, qword ptr ds:[r14+0x28]
0x48, 0x83, 0xC0, 0x16, // add rax, 0x16
0x48, 0x8B, 0x00, // mov rax, qword ptr ds:[rax]
0x48, 0x8B, 0x1D, 0xDC, 0xFF, 0xFF, 0xFF, // mov rbx, [string1]
0x48, 0x39, 0xC3, // cmp rbx,rax
0x74, 0x15, // je orig
0x48, 0x8B, 0x1D, 0xD8, 0xFF, 0xFF, 0xFF, // mov rbx, [string2]
0x48, 0x39, 0xC3, // cmp rbx,rax
0x74, 0x09, // je orig
0x66, 0xC7, 0x05, 0xC0, 0xFF, 0xFF, 0xFF, 0x01, 0x00, // mov word ptr ds:[7FF78D6366BD],1 (set isLoadingLevel to 1)
// original instructions
0x5b, // pop rbx
0x58, // pop rax
0x45, 0x33, 0xC9, // xor r9d, r9d
0x48, 0x8B, 0xD7, // mov rdx, rdi
0x49, 0x8B, 0xCC, // mov rcx, r12
0x41, 0x5c,
0x5F,
0xFF, 0x93, 0x70, 0x04, 0x00, 0x00, // call qword ptr ds:[rbx+470] - assume 470 for all versions but it could change with a different engine
0x57,
0x41, 0x54,
0x66, 0xC7, 0x05, 0xA0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, // mov word ptr ds:[7FF78D6366BD],1 (set isLoadingLevel to 0)
0x84, 0xC0, // test al, al
0x49, 0x8B, 0xCE, // mov rcx, r14
0x40, 0x0F, 0x94, 0xC6, // sete sil
0xC3 // ret
};
// bytes to detour load start function
var isLoadingLevelHookBytes = new List<byte>()
{
0x50, // push rax
0x48, 0xB8 // mov rax, jumploc
};
isLoadingLevelHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.isLoadingLevelDetour));
isLoadingLevelHookBytes.AddRange(new byte[] {
0xFF, 0xD0, // call rax
0x58, // pop rax
0x90, 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90
});
// Place hook on FLatentActionManager::AddNewAction
IntPtr ptrIsExitingZoneAddr = scanner.Scan(new SigScanTarget(0, // target the 0th bytes
"48 83 EC 40",// sub rsp, 0x40
"48 8B D9", // mov rbx, rcx
"48 8B EA", // mov rbp, rdx
"48 8D 4C 24 20" // lea rcx, ss:[rsp+0x20]
));
if (ptrIsExitingZoneAddr == IntPtr.Zero)
{
throw new Exception("Could not find ptrIsExitingZoneAddr detour!");
}
// isExitingZone == 1 means exiting zone
// isExitingZone == 0 means not exiting zone
vars.isExitingZone = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for variable
memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 0); // Set isExitingZone to 0 which is the default "not exiting" state
memory.WriteValue<ulong>((IntPtr)vars.isExitingZone + 2, (ulong)vars.arrUUIDs); // Set isExitingZone to 0 which is the default "not exiting" state
vars.isExitingZoneDetour = vars.isExitingZone + 2 + 8; // skip over the first 2 bytes plus 8 for our array address
vars.watchers.Add(new MemoryWatcher<bool>(new DeepPointer((IntPtr)vars.isExitingZone)){ Name = "isExitingZone" });
var isExitingZoneDetourBytes = new byte[]
{
0x53,//push rbx
0x50,//push rax
0x48, 0x31, 0xc0,// xor rax, rax
0x48, 0x8b, 0x1d, 0xEC, 0xFF, 0xFF, 0xFF,//mov rbx, array
// loop
0x48, 0x8b, 0x3c, 0x03,// mov rdi, [rbx + rax]
0x48, 0x83, 0xff, 0x00,// cmp rdi, 0
0x74, 0x14,// je end
0x4c, 0x39, 0xc7,// cmp rdi, r8
0x74, 0x06,//je set
0x48, 0x83, 0xc0, 0x08,// add rax,8
0xeb, 0xeb,//jmp loop
0x66, 0xC7, 0x05, 0xCC, 0xFF, 0xFF, 0xFF, 0x01, 0x00, // mov word ptr ds:[7FF78D6366BD],1 (set isExitingZone to 1)
0x58, //pop rax
0x5b, //pop rbx
// original instructions
0x58, //pop rax
0x48, 0x83, 0xEC, 0x40,// sub rsp, 0x40
0x48, 0x8B, 0xD9, // mov rbx, rcx
0x48, 0x8B, 0xEA, // mov rbp, rdx
0x48, 0x8D, 0x4C, 0x24, 0x20, // lea rcx, ss:[rsp+0x20]
0x50,//push rax
0xC3 // ret
};
// bytes to detour load start function
var isExitingZoneHookBytes = new List<byte>()
{
//0x57, // push rdi
0x48, 0xB8 // mov rax, jumploc
};
isExitingZoneHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.isExitingZoneDetour));
isExitingZoneHookBytes.AddRange(new byte[] {
0xFF, 0xD0, // call rdi
//0x5F, // pop rdi
0x90, 0x90, 0x90
});
// suspend game while writing so it doesn't crash
try
{
// write the detour code at the allocated memory address
game.WriteBytes((IntPtr)vars.isLoadingLevelDetour, isLoadingLevelDetourBytes);
game.WriteBytes((IntPtr)vars.isExitingZoneDetour, isExitingZoneDetourBytes);
game.WriteBytes((IntPtr)vars.isRestartingDetour, isRestartingDetourBytes);
// write detour calls
game.WriteBytes(ptrisLoadingLevelAddr, isLoadingLevelHookBytes.ToArray());
game.WriteBytes(ptrIsExitingZoneAddr, isExitingZoneHookBytes.ToArray());
game.WriteBytes(ptrIsRestartingAddr, isRestartingHookBytes.ToArray());
}
catch
{
vars.FreeMemory(game);
throw;
}
finally
{
game.Resume();
}
// Get isSeamlessTravel (UWorld::Tick variable)
IntPtr ptrIsSeamlessTravel = scanner.Scan(new SigScanTarget(0, // target the 0th byte
"48 83 EC 28", // sub rsp, 0x28
"48 8B D1", // mov rdx, rcx
"48 8B 0D ?? ?? ?? ??", // mov rcx, qword ptr ds:[0x????]
"E8 ?? ?? ?? ??", // call ??
"0F B6 80 90 00 00 00" // movzx eax, byte ptr ds:[rax+0x90]
));
if (ptrIsSeamlessTravel == IntPtr.Zero)
{
throw new Exception("Could not find ptrIsSeamlessTravel!");
}
ulong instrAddrRst = (ulong)ptrIsSeamlessTravel;
ulong nextInstrAddrRst = (ulong)ptrIsSeamlessTravel + 14; // mov is referenced from the instruction after the mov
ulong instrOperandRst = game.ReadValue<uint>((IntPtr)instrAddrRst + 10); // We only want 4 bytes in "48 8d 3d ?? ?? ?? ??"
ulong isSeamlessTravelBase = (nextInstrAddrRst + instrOperandRst) - (ulong)game.MainModule.BaseAddress;
ulong readAddr = (ulong)game.MainModule.BaseAddress + isSeamlessTravelBase;
vars.watchers.Add(new MemoryWatcher<bool>(new DeepPointer((IntPtr)readAddr,0xc38,0x0, 0x98)){ Name = "isSeamlessTravel" });
}
update
{
vars.watchers.UpdateAll(game);
var isLoadingLevel = !vars.watchers["isLoadingLevel"].Current && vars.watchers["isLoadingLevel"].Old;
var isSeamlessTravel = !vars.watchers["isSeamlessTravel"].Current && vars.watchers["isSeamlessTravel"].Old;
if(isLoadingLevel || isSeamlessTravel)
{
// Finished loading or restarting
memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 0); // Set isExitingZone to 0
memory.WriteValue<byte>((IntPtr)vars.isRestarting, 0); // Set isRestarting to 0
}
}
start
{
var isLoadingLevel = !vars.watchers["isLoadingLevel"].Current && vars.watchers["isLoadingLevel"].Old;
var isSeamlessTravel = !vars.watchers["isSeamlessTravel"].Current && vars.watchers["isSeamlessTravel"].Old;
return isLoadingLevel || isSeamlessTravel;
}
split
{
var isExitingZone = vars.watchers["isExitingZone"].Current && !vars.watchers["isExitingZone"].Old;
return isExitingZone;
}
isLoading
{
if(vars.watchers["isRestarting"].Current && settings["disable_restart_time_removal"])
{
return false;
}
return vars.watchers["isLoadingLevel"].Current || vars.watchers["isSeamlessTravel"].Current || vars.watchers["isExitingZone"].Current;
}
shutdown
{
}