about summary refs log tree commit diff stats
path: root/TheLooker.asl
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2022-08-19 19:51:42 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2022-08-19 19:51:42 -0400
commit060b8b27ed16943339ab241413d5f4f1073dee0e (patch)
tree8094da7cbff78108bc50cd51212bf4433fcd2335 /TheLooker.asl
parent9e66ee6b3c375252562ff4df6c91e89781a586da (diff)
downloadautosplitters-060b8b27ed16943339ab241413d5f4f1073dee0e.tar.gz
autosplitters-060b8b27ed16943339ab241413d5f4f1073dee0e.tar.bz2
autosplitters-060b8b27ed16943339ab241413d5f4f1073dee0e.zip
[The Looker] Create autosplitter
It uses code injection to get the game to report the most recently obtained achievement. By default it will only split on The Obelisk (the Any% ending), but there is a setting to have it split on all achievements.

It does not auto-start yet.
Diffstat (limited to 'TheLooker.asl')
-rw-r--r--TheLooker.asl203
1 files changed, 203 insertions, 0 deletions
diff --git a/TheLooker.asl b/TheLooker.asl new file mode 100644 index 0000000..aad9d7f --- /dev/null +++ b/TheLooker.asl
@@ -0,0 +1,203 @@
1// Autosplitter script for The Looker 2022-06-28.
2//
3// Written by hatkirby, with inspiration from CaptainRektbeard's Marble Marcher
4// autosplitter.
5
6state("The Looker") {}
7
8startup {
9 // For logging!
10 vars.Log = (Action<object>)((output) => print("[The Looker ASL] " + output));
11
12 // Function for deallocating memory used by this process.
13 vars.FreeMemory = (Action<Process>)(p => {
14 vars.Log("Deallocating");
15 foreach (IDictionary<string, object> hook in vars.hooks){
16 if(((bool)hook["enabled"]) == false){
17 continue;
18 }
19 p.FreeMemory((IntPtr)hook["outputPtr"]);
20 p.FreeMemory((IntPtr)hook["funcPtr"]);
21 }
22 });
23
24 vars.hooks = new List<ExpandoObject> {
25 (vars.unlockAchievement = new ExpandoObject()),
26 };
27
28 // The unlockAchievement function will give us a pointer to the most
29 // recently unlocked achievement.
30 vars.unlockAchievement.name = "UnlockAchievement";
31 vars.unlockAchievement.outputSize = 8;
32 vars.unlockAchievement.overwriteBytes = 11;
33 vars.unlockAchievement.payload = new byte[] { 0x49, 0x89, 0x08 }; // mov [r8], rcx
34 vars.unlockAchievement.enabled = true;
35 vars.unlockAchievement.offset = 0x96D700;
36
37 // If this isn't checked, then it will only split on The Obelisk.
38 settings.Add("allachs", false, "Split on every achievement");
39}
40
41init {
42 // Install hooks
43 IntPtr baseAddress = modules.Where(m => m.ModuleName == "UnityPlayer.dll").First().BaseAddress;
44 foreach (IDictionary<string, object> hook in vars.hooks)
45 {
46 if(((bool)hook["enabled"]) == false){
47 continue;
48 }
49 vars.Log("Installing hook for " + hook["name"]);
50
51 // Get pointer to function
52 hook["injectPtr"] = baseAddress + (int)hook["offset"];
53
54 // Find nearby 14 byte code cave to store long jmp
55 int caveSize = 0;
56 int dist = 0;
57 hook["cavePtr"] = IntPtr.Zero;
58 vars.Log("Scanning for code cave");
59 for(int i=1;i<0xFFFFFFFF;i++){
60 try {
61 byte b = game.ReadBytes((IntPtr)hook["injectPtr"] + i, 1)[0];
62 if (b == 0xCC){
63 caveSize++;
64 if (caveSize == 14){
65 hook["caveOffset"] = i - 11;
66 hook["cavePtr"] = (IntPtr)hook["injectPtr"] + (int)hook["caveOffset"];
67 break;
68 }
69 }else{
70 caveSize = 0;
71 }
72 } catch {
73 caveSize = 0;
74 }
75 }
76 if ((IntPtr)hook["cavePtr"] == IntPtr.Zero){
77 throw new Exception("Unable to locate nearby code cave");
78 }
79 vars.Log("Found cave " + ((int)hook["caveOffset"]).ToString("X") + " bytes away");
80
81 // Allocate memory for output
82 hook["outputPtr"] = game.AllocateMemory((int)hook["outputSize"]);
83
84 // Build the hook function
85 var funcBytes = new List<byte>() { 0x49, 0xB8 }; // mov r8, ...
86 funcBytes.AddRange(BitConverter.GetBytes((UInt64)((IntPtr)hook["outputPtr"]))); // ...outputPtr
87 funcBytes.AddRange((byte[])hook["payload"]);
88
89 // Allocate memory to store the function
90 hook["funcPtr"] = game.AllocateMemory(funcBytes.Count + (int)hook["overwriteBytes"] + 14);
91
92 // Write the detour:
93 // - Copy bytes from the start of original function which will be overwritten
94 // - Overwrite those bytes with a 5 byte jump instruction to a nearby code cave
95 // - In the code cave, write a 14 byte jump to the memory allocated for our hook function
96 // - Write the hook function
97 // - Write a copy of the overwritten code at the end of the hook function
98 // - Following this, write a jump back the original function
99 game.Suspend();
100 try {
101 // Copy the bytes which will be overwritten
102 byte[] overwritten = game.ReadBytes((IntPtr)hook["injectPtr"], (int)hook["overwriteBytes"]);
103
104 // Write short jump to code cave
105 List<byte> caveJump = new List<byte>() { 0xE9 }; // jmp ...
106 caveJump.AddRange(BitConverter.GetBytes((int)hook["caveOffset"] - 5)); // ...caveOffset - 5
107 game.WriteBytes((IntPtr)hook["injectPtr"], caveJump.ToArray());
108 hook["origBytes"] = overwritten;
109
110 // NOP out excess bytes
111 for(int i=0;i<(int)hook["overwriteBytes"]-5;i++){
112 game.WriteBytes((IntPtr)hook["injectPtr"] + 5 + i, new byte[] { 0x90 });
113 }
114
115 // Write jump to hook function in code cave
116 List<byte> firstJump = new List<byte>() { 0x49, 0xb8 }; // mov r8, ...
117 firstJump.AddRange(BitConverter.GetBytes((long)(IntPtr)hook["funcPtr"])); // ...funcPtr
118 firstJump.AddRange(new byte[] { 0x41, 0xff, 0xe0 }); // jmp r8
119 game.WriteBytes((IntPtr)hook["cavePtr"], firstJump.ToArray());
120
121 // Write the hook function
122 game.WriteBytes((IntPtr)hook["funcPtr"], funcBytes.ToArray());
123
124 // Write the overwritten code
125 game.WriteBytes((IntPtr)hook["funcPtr"] + funcBytes.Count, overwritten);
126
127 // Write the jump to the original function
128 List<byte> secondJump = new List<byte>() { 0x49, 0xb8 }; // mov r8, ...
129 secondJump.AddRange(BitConverter.GetBytes((long)((IntPtr)hook["injectPtr"] + (int)hook["overwriteBytes"]))); // ...funcPtr
130 secondJump.AddRange(new byte[] { 0x41, 0xff, 0xe0 }); // jmp r8
131 game.WriteBytes((IntPtr)hook["funcPtr"] + funcBytes.Count + (int)hook["overwriteBytes"], secondJump.ToArray());
132 }
133 catch {
134 vars.FreeMemory(game);
135 throw;
136 }
137 finally{
138 game.Resume();
139 }
140
141 // Calcuate offset of injection point from module base address
142 UInt64 offset = (UInt64)((IntPtr)hook["injectPtr"]) - (UInt64)baseAddress;
143
144 vars.Log("Output: " + ((IntPtr)hook["outputPtr"]).ToString("X"));
145 vars.Log("Injection: " + ((IntPtr)hook["injectPtr"]).ToString("X") + " (UnityPlayer.dll+" + offset.ToString("X") + ")");
146 vars.Log("Function: " + ((IntPtr)hook["funcPtr"]).ToString("X"));
147 }
148
149 vars.Watchers = new MemoryWatcherList
150 {
151 (vars.lastAchievement = new MemoryWatcher<IntPtr>((IntPtr)vars.unlockAchievement.outputPtr))
152 };
153}
154
155update
156{
157 vars.Watchers.UpdateAll(game);
158}
159
160split {
161 if (vars.lastAchievement.Current != vars.lastAchievement.Old) {
162 string result;
163 game.ReadString((IntPtr)(vars.lastAchievement.Current + 0x14), ReadStringType.UTF16, 40, out result);
164 vars.Log(result);
165
166 return (settings["allachs"] || result == "ACH_LAST_PUZZLE");
167 }
168}
169
170shutdown
171{
172 if (game == null)
173 return;
174
175 game.Suspend();
176 try
177 {
178 vars.Log("Restoring memory");
179 foreach (IDictionary<string, object> hook in vars.hooks){
180 if(((bool)hook["enabled"]) == false){
181 continue;
182 }
183 // Restore overwritten bytes
184 game.WriteBytes((IntPtr)hook["injectPtr"], (byte[])hook["origBytes"]);
185
186 // Remove jmp from code cave
187 for(int i=0;i<12;i++){
188 game.WriteBytes((IntPtr)hook["cavePtr"] + i, new byte[] { 0xCC });
189 }
190
191 }
192 vars.Log("Memory restored");
193 }
194 catch
195 {
196 throw;
197 }
198 finally
199 {
200 game.Resume();
201 vars.FreeMemory(game);
202 }
203}