diff options
author | Star Rauchenberger <fefferburbia@gmail.com> | 2022-08-19 19:51:42 -0400 |
---|---|---|
committer | Star Rauchenberger <fefferburbia@gmail.com> | 2022-08-19 19:51:42 -0400 |
commit | 060b8b27ed16943339ab241413d5f4f1073dee0e (patch) | |
tree | 8094da7cbff78108bc50cd51212bf4433fcd2335 | |
parent | 9e66ee6b3c375252562ff4df6c91e89781a586da (diff) | |
download | autosplitters-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.
-rw-r--r-- | TheLooker.asl | 203 |
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 | |||
6 | state("The Looker") {} | ||
7 | |||
8 | startup { | ||
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 | |||
41 | init { | ||
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 | |||
155 | update | ||
156 | { | ||
157 | vars.Watchers.UpdateAll(game); | ||
158 | } | ||
159 | |||
160 | split { | ||
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 | |||
170 | shutdown | ||
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 | } | ||