From 060b8b27ed16943339ab241413d5f4f1073dee0e Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 19 Aug 2022 19:51:42 -0400 Subject: [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. --- TheLooker.asl | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 TheLooker.asl diff --git a/TheLooker.asl b/TheLooker.asl new file mode 100644 index 0000000..aad9d7f --- /dev/null +++ b/TheLooker.asl @@ -0,0 +1,203 @@ +// Autosplitter script for The Looker 2022-06-28. +// +// Written by hatkirby, with inspiration from CaptainRektbeard's Marble Marcher +// autosplitter. + +state("The Looker") {} + +startup { + // For logging! + vars.Log = (Action)((output) => print("[The Looker ASL] " + output)); + + // Function for deallocating memory used by this process. + vars.FreeMemory = (Action)(p => { + vars.Log("Deallocating"); + foreach (IDictionary hook in vars.hooks){ + if(((bool)hook["enabled"]) == false){ + continue; + } + p.FreeMemory((IntPtr)hook["outputPtr"]); + p.FreeMemory((IntPtr)hook["funcPtr"]); + } + }); + + vars.hooks = new List { + (vars.unlockAchievement = new ExpandoObject()), + }; + + // The unlockAchievement function will give us a pointer to the most + // recently unlocked achievement. + vars.unlockAchievement.name = "UnlockAchievement"; + vars.unlockAchievement.outputSize = 8; + vars.unlockAchievement.overwriteBytes = 11; + vars.unlockAchievement.payload = new byte[] { 0x49, 0x89, 0x08 }; // mov [r8], rcx + vars.unlockAchievement.enabled = true; + vars.unlockAchievement.offset = 0x96D700; + + // If this isn't checked, then it will only split on The Obelisk. + settings.Add("allachs", false, "Split on every achievement"); +} + +init { + // Install hooks + IntPtr baseAddress = modules.Where(m => m.ModuleName == "UnityPlayer.dll").First().BaseAddress; + foreach (IDictionary hook in vars.hooks) + { + if(((bool)hook["enabled"]) == false){ + continue; + } + vars.Log("Installing hook for " + hook["name"]); + + // Get pointer to function + hook["injectPtr"] = baseAddress + (int)hook["offset"]; + + // Find nearby 14 byte code cave to store long jmp + int caveSize = 0; + int dist = 0; + hook["cavePtr"] = IntPtr.Zero; + vars.Log("Scanning for code cave"); + for(int i=1;i<0xFFFFFFFF;i++){ + try { + byte b = game.ReadBytes((IntPtr)hook["injectPtr"] + i, 1)[0]; + if (b == 0xCC){ + caveSize++; + if (caveSize == 14){ + hook["caveOffset"] = i - 11; + hook["cavePtr"] = (IntPtr)hook["injectPtr"] + (int)hook["caveOffset"]; + break; + } + }else{ + caveSize = 0; + } + } catch { + caveSize = 0; + } + } + if ((IntPtr)hook["cavePtr"] == IntPtr.Zero){ + throw new Exception("Unable to locate nearby code cave"); + } + vars.Log("Found cave " + ((int)hook["caveOffset"]).ToString("X") + " bytes away"); + + // Allocate memory for output + hook["outputPtr"] = game.AllocateMemory((int)hook["outputSize"]); + + // Build the hook function + var funcBytes = new List() { 0x49, 0xB8 }; // mov r8, ... + funcBytes.AddRange(BitConverter.GetBytes((UInt64)((IntPtr)hook["outputPtr"]))); // ...outputPtr + funcBytes.AddRange((byte[])hook["payload"]); + + // Allocate memory to store the function + hook["funcPtr"] = game.AllocateMemory(funcBytes.Count + (int)hook["overwriteBytes"] + 14); + + // Write the detour: + // - Copy bytes from the start of original function which will be overwritten + // - Overwrite those bytes with a 5 byte jump instruction to a nearby code cave + // - In the code cave, write a 14 byte jump to the memory allocated for our hook function + // - Write the hook function + // - Write a copy of the overwritten code at the end of the hook function + // - Following this, write a jump back the original function + game.Suspend(); + try { + // Copy the bytes which will be overwritten + byte[] overwritten = game.ReadBytes((IntPtr)hook["injectPtr"], (int)hook["overwriteBytes"]); + + // Write short jump to code cave + List caveJump = new List() { 0xE9 }; // jmp ... + caveJump.AddRange(BitConverter.GetBytes((int)hook["caveOffset"] - 5)); // ...caveOffset - 5 + game.WriteBytes((IntPtr)hook["injectPtr"], caveJump.ToArray()); + hook["origBytes"] = overwritten; + + // NOP out excess bytes + for(int i=0;i<(int)hook["overwriteBytes"]-5;i++){ + game.WriteBytes((IntPtr)hook["injectPtr"] + 5 + i, new byte[] { 0x90 }); + } + + // Write jump to hook function in code cave + List firstJump = new List() { 0x49, 0xb8 }; // mov r8, ... + firstJump.AddRange(BitConverter.GetBytes((long)(IntPtr)hook["funcPtr"])); // ...funcPtr + firstJump.AddRange(new byte[] { 0x41, 0xff, 0xe0 }); // jmp r8 + game.WriteBytes((IntPtr)hook["cavePtr"], firstJump.ToArray()); + + // Write the hook function + game.WriteBytes((IntPtr)hook["funcPtr"], funcBytes.ToArray()); + + // Write the overwritten code + game.WriteBytes((IntPtr)hook["funcPtr"] + funcBytes.Count, overwritten); + + // Write the jump to the original function + List secondJump = new List() { 0x49, 0xb8 }; // mov r8, ... + secondJump.AddRange(BitConverter.GetBytes((long)((IntPtr)hook["injectPtr"] + (int)hook["overwriteBytes"]))); // ...funcPtr + secondJump.AddRange(new byte[] { 0x41, 0xff, 0xe0 }); // jmp r8 + game.WriteBytes((IntPtr)hook["funcPtr"] + funcBytes.Count + (int)hook["overwriteBytes"], secondJump.ToArray()); + } + catch { + vars.FreeMemory(game); + throw; + } + finally{ + game.Resume(); + } + + // Calcuate offset of injection point from module base address + UInt64 offset = (UInt64)((IntPtr)hook["injectPtr"]) - (UInt64)baseAddress; + + vars.Log("Output: " + ((IntPtr)hook["outputPtr"]).ToString("X")); + vars.Log("Injection: " + ((IntPtr)hook["injectPtr"]).ToString("X") + " (UnityPlayer.dll+" + offset.ToString("X") + ")"); + vars.Log("Function: " + ((IntPtr)hook["funcPtr"]).ToString("X")); + } + + vars.Watchers = new MemoryWatcherList + { + (vars.lastAchievement = new MemoryWatcher((IntPtr)vars.unlockAchievement.outputPtr)) + }; +} + +update +{ + vars.Watchers.UpdateAll(game); +} + +split { + if (vars.lastAchievement.Current != vars.lastAchievement.Old) { + string result; + game.ReadString((IntPtr)(vars.lastAchievement.Current + 0x14), ReadStringType.UTF16, 40, out result); + vars.Log(result); + + return (settings["allachs"] || result == "ACH_LAST_PUZZLE"); + } +} + +shutdown +{ + if (game == null) + return; + + game.Suspend(); + try + { + vars.Log("Restoring memory"); + foreach (IDictionary hook in vars.hooks){ + if(((bool)hook["enabled"]) == false){ + continue; + } + // Restore overwritten bytes + game.WriteBytes((IntPtr)hook["injectPtr"], (byte[])hook["origBytes"]); + + // Remove jmp from code cave + for(int i=0;i<12;i++){ + game.WriteBytes((IntPtr)hook["cavePtr"] + i, new byte[] { 0xCC }); + } + + } + vars.Log("Memory restored"); + } + catch + { + throw; + } + finally + { + game.Resume(); + vars.FreeMemory(game); + } +} -- cgit 1.4.1