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 | } | ||
