state("Amnesia","Steam 1.5") { byte loading1 : 0x75A120, 0x84, 0x7C, 0x04; byte loading2 : 0x75A120, 0x84, 0x7C; string32 audio : 0x75A108, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x75A108, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x741A34, 0x94, 0x60, 0x38; bool pActive : 0x741A34, 0xBC, 0x58; float pMSMul : 0x741A34, 0xBC, 0xD4; float pPosX : 0x741A34, 0xBC, 0x54, 0x48; } state("Amnesia_NoSteam","NoSteam 1.5") { byte loading1 : 0x754CF0, 0x84, 0x7C, 0x04; byte loading2 : 0x754CF0, 0x84, 0x7C; string32 audio : 0x754CD8, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x754CD8, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x73C604, 0x94, 0x60, 0x38; bool pActive : 0x73C604, 0xBC, 0x58; float pMSMul : 0x73C604, 0xBC, 0xD4; float pPosX : 0x73C604, 0xBC, 0x54, 0x48; } state("Amnesia","1.42") { byte loading1 : 0x7131C0, 0x84, 0x7C, 0x04; byte loading2 : 0x7131C0, 0x84, 0x7C; string32 audio : 0x7131A8, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x7131A8, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x6FA874, 0x5C, 0x60, 0x38; bool pActive : 0x6FA874, 0x84, 0x58; float pMSMul : 0x6FA874, 0x84, 0xD4; float pPosX : 0x6FA874, 0x84, 0x54, 0x48; } state("Amnesia_NoSteam","NoSteam 1.42") { byte loading1 : 0x7131C0, 0x84, 0x7C, 0x04; byte loading2 : 0x7131C0, 0x84, 0x7C; string32 audio : 0x7131A8, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x7131A8, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x6FA874, 0x5C, 0x60, 0x38; bool pActive : 0x6FA874, 0x84, 0x58; float pMSMul : 0x6FA874, 0x84, 0xD4; float pPosX : 0x6FA874, 0x84, 0x54, 0x48; } state("Amnesia","Steam 1.42") { byte loading1 : 0x781320, 0x84, 0x7C, 0x04; byte loading2 : 0x781320, 0x84, 0x7C; string32 audio : 0x781308, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x781308, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x768C54, 0x5C, 0x60, 0x38; bool pActive : 0x768C54, 0x84, 0x58; float pMSMul : 0x768C54, 0x84, 0xD4; float pPosX : 0x768C54, 0x84, 0x54, 0x48; } state("Amnesia","Streamnesia 2.1") { byte loading1 : 0x714EB0, 0x84, 0x7C, 0x04; byte loading2 : 0x714EB0, 0x84, 0x7C; string32 audio : 0x714E98, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x714E98, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x6FC964, 0x5C, 0x60, 0x38; bool pActive : 0x6FC964, 0x84, 0x58; float pMSMul : 0x6FC964, 0x84, 0xD4; float pPosX : 0x6FC964, 0x84, 0x54, 0x48; } state("Amnesia_NoSteam","NoSteam 1.43") { byte loading1 : 0x788450, 0x84, 0x7C, 0x04; byte loading2 : 0x788450, 0x84, 0x7C; string32 audio : 0x788438, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x788438, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x76FD64, 0x5C, 0x60, 0x38; bool pActive : 0x76FD64, 0x84, 0x58; float pMSMul : 0x76FD64, 0x84, 0xD4; float pPosX : 0x76FD64, 0x84, 0x54, 0x48; } state("Amnesia","Steam 1.43") { byte loading1 : 0x78D880, 0x84, 0x7C, 0x04; byte loading2 : 0x78D880, 0x84, 0x7C; string32 audio : 0x78D868, 0x48, 0x38, 0x04, 0x08, 0x04, 0x00; string15 audio2 : 0x78D868, 0x48, 0x38, 0x04, 0x08, 0x04; string24 map : 0x775194, 0x5C, 0x60, 0x38; bool pActive : 0x775194, 0x84, 0x58; float pMSMul : 0x775194, 0x84, 0xD4; float pPosX : 0x775194, 0x84, 0x54, 0x48; } startup { vars.aslName = "AmnesiaASL TDD"; if(timer.CurrentTimingMethod == TimingMethod.RealTime){ var timingMessage = MessageBox.Show( "This game uses Game Time (time without loads) as the main timing method.\n"+ "LiveSplit is currently set to show Real Time (time INCLUDING loads).\n"+ "Would you like the timing method to be set to Game Time for you?", vars.aslName+" | LiveSplit", MessageBoxButtons.YesNo,MessageBoxIcon.Question ); if (timingMessage == DialogResult.Yes) timer.CurrentTimingMethod = TimingMethod.GameTime; } // Stores previous map vars.lastMap = ""; vars.injected = false; settings.Add("fullSplit",true,"Split on level changes (If disabled, will only auto-start and auto-end)"); vars.log = (Action)((lvl,text) => { print("["+vars.aslName+"] "+lvl+": "+text.Replace("-"," ")); }); // Copy bytes from one address to another // Params: game process, address of bytes to copy, length of bytes to copy, address to copy bytes to // Returns: boolean result of WriteBytes vars.CopyMemory = (Func)((proc, src, len, dest) => { var bytes = proc.ReadBytes(src, len); return proc.WriteBytes(dest, bytes); }); // Write MOV instruction // Params: game process, address to write instruction to, length of bytes to overwrite, address to change byte at, byte to set // Returns: boolean result of WriteBytes vars.WriteMov = (Func)((proc, src, len, dest, val) => { var bytes = proc.ReadBytes(src, len); var newBytes = new List(new byte[] { 0xC6, 0x05 }); var address = BitConverter.GetBytes((int) dest); newBytes.AddRange(address); newBytes.AddRange(val); // nop the leftover bytes int extraBytes = len - newBytes.Count(); if (extraBytes > 0) { var nops = Enumerable.Repeat((byte) 0x90, extraBytes).ToArray(); newBytes.AddRange(nops); } return proc.WriteBytes(src, newBytes.ToArray()); }); // AOB signature for Gametime, found (somewhat) near string "Game Running". Use the address at 8B vars.sigGametime = new SigScanTarget(0, "8B 4E 68 C6 45 D3 01"); // AOB signature for Mapload, found near string "OnLeave". Use the address at A1 vars.sigMapload = new SigScanTarget(3, "89 45 ?? A1 ?? ?? ?? ?? 8B 88"); // AOB signature for Menu, found near string "menu_loading_screen.jpg". Use the address at 8B vars.sigMenuload = new SigScanTarget(0, "8B ?? 70 8B ?? 30 6?"); } shutdown { if(game != null && vars.injected) { // Replace injected code with the original code game.Suspend(); vars.log("DEBUG","Restoring original code"); vars.CopyMemory(game, (IntPtr) vars.origGametime, 7, (IntPtr) vars.ptrGametime); vars.log("DEBUG","Bytes at Gametime address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrGametime, 7)).Replace("-"," ")); vars.CopyMemory(game, (IntPtr) vars.origMapload, 5, (IntPtr) vars.ptrMapload); vars.log("DEBUG","Bytes at Mapload address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrMapload, 5)).Replace("-"," ")); vars.CopyMemory(game, (IntPtr) vars.origMenu, 6, (IntPtr) vars.ptrMenuload); vars.log("DEBUG","Bytes at Menu address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrMenuload, 6)).Replace("-"," ")); game.FreeMemory((IntPtr) vars.aslMem); vars.injected = false; vars.log("DEBUG","Restored original code"); game.Resume(); } } init { var module = modules.First(); var name = module.ModuleName.ToLower(); // Fix for rare occasions when NTDLL is loaded first if(!name.Contains("amnesia")) return; var baseAddr = module.BaseAddress; var size = module.ModuleMemorySize; vars.lastMap = ""; vars.injected = false; switch(size) { case 8208384: version = "Steam 1.5"; // Bytes at the inject point were changed, luckily the byte combo is unique vars.sigMenuload = new SigScanTarget(0, "8B ?? ?? ?? ?? ?? 8B 42 30 6A 17"); break; case 8187904: version = "NoSteam 1.5"; // Bytes at the inject point were changed, luckily the byte combo is unique vars.sigMenuload = new SigScanTarget(0, "8B ?? ?? ?? ?? ?? 8B 42 30 6A 17"); break; case 8421376: version = "Steam 1.43"; break; case 8396800: version = "NoSteam 1.43"; break; case 7884800: version = "Streamnesia 2.1"; break; case 7872512: version = name == "amnesia.exe" ? "DRM-free 1.42" : "NoSteam 1.42"; break; case 8368128: version = "Steam 1.42"; break; default: version = "Unknown"; var gameMessageText = name+"="+size; var gameMessage = MessageBox.Show( "It appears you're running an unknown version of the game.\n\n"+ "Please @PrototypeAlpha#7561 on the HPL Games Speedrunning discord with "+ "the following:\n"+gameMessageText+"\n\n"+ "Press OK to copy the above info to the clipboard and close this message.", vars.aslName+" | LiveSplit", MessageBoxButtons.OKCancel,MessageBoxIcon.Warning ); if (gameMessage == DialogResult.OK) Clipboard.SetText(gameMessageText); break; } vars.log("INFO",size+" = " + version); var scanner = new SignatureScanner(game, baseAddr, size); // Scan memory for Gametime signature IntPtr staticGametime = vars.ptrGametime = scanner.Scan((SigScanTarget) vars.sigGametime); // Scan memory for Mapload signature IntPtr staticMapload = vars.ptrMapload = scanner.Scan((SigScanTarget) vars.sigMapload); // Scan memory for Menuload signature IntPtr staticMenuload = vars.ptrMenuload = scanner.Scan((SigScanTarget) vars.sigMenuload); // Allocate memory for our code instead of looking for a code cave vars.log("DEBUG","Allocating memory..."); var aslMem = vars.aslMem = game.AllocateMemory(64); var addrMem = BitConverter.GetBytes((int) aslMem).Reverse().ToArray(); vars.log("DEBUG","aslMem address: "+BitConverter.ToString(addrMem).Replace("-","")); if(staticGametime == IntPtr.Zero || staticMapload == IntPtr.Zero || staticMenuload == IntPtr.Zero){ vars.log("ERROR","Can't find signatures. Unknown game version?"); game.FreeMemory((IntPtr) aslMem); MessageBox.Show( "Error: Can't find signatures.\n"+ "\n staticGametime = "+staticGametime+ "\n staticMapload = "+staticMapload+ "\n staticMenuload = "+staticMenuload+ "\n\nTry restarting the game.", vars.aslName+" | LiveSplit", MessageBoxButtons.OK,MessageBoxIcon.Error ); } else { vars.log("INFO","Starting inject"); game.Suspend(); // Set loading var to 1 in case we're just starting up the game game.WriteBytes((IntPtr)aslMem, new byte[] {1}); var addrGametime = BitConverter.GetBytes((int) staticGametime).Reverse().ToArray(); vars.log("DEBUG","Gametime address: "+BitConverter.ToString(addrGametime).Replace("-","")); vars.log("DEBUG","Original bytes at Gametime address: "+BitConverter.ToString(game.ReadBytes(staticGametime, 7)).Replace("-"," ")); IntPtr codeGametime = aslMem+2; vars.WriteMov(game, codeGametime, 7, aslMem, new byte[] {0}); // Write instruction to set isLoading to 0 vars.CopyMemory(game, staticGametime, 7, codeGametime+7); // Write original code game.WriteJumpInstruction(codeGametime+7+7, staticGametime+7); // Write jump out game.WriteJumpInstruction(staticGametime, codeGametime); // Write jump in var addrMapload = BitConverter.GetBytes((int) staticMapload).Reverse().ToArray(); vars.log("DEBUG","staticMapload address: "+BitConverter.ToString(addrMapload).Replace("-","")); vars.log("DEBUG","Original bytes at staticMapload address: "+BitConverter.ToString(game.ReadBytes(staticMapload, 5)).Replace("-"," ")); IntPtr codeMapload = codeGametime+7+7+5; vars.WriteMov(game, codeMapload, 7, aslMem, new byte[] {1}); // Write instruction to set isLoading to 1 vars.CopyMemory(game, staticMapload, 5, codeMapload+7); // Write original code game.WriteJumpInstruction(codeMapload+7+5, staticMapload+5); // Write jump out game.WriteJumpInstruction(staticMapload, codeMapload); // Write jump in var addrMenuload = BitConverter.GetBytes((int) staticMenuload).Reverse().ToArray(); vars.log("DEBUG","Menu address: "+BitConverter.ToString(addrMenuload).Replace("-","")); vars.log("DEBUG","Original bytes at Menu address: "+BitConverter.ToString(game.ReadBytes(staticMenuload, 6)).Replace("-"," ")); IntPtr codeMenuload = codeMapload+7+5+5; vars.WriteMov(game, codeMenuload, 6, aslMem, new byte[] {1}); // Write instruction to set isLoading to 1 vars.CopyMemory(game, staticMenuload, 6, codeMenuload+7); // Write original code game.WriteJumpInstruction(codeMenuload+7+6, staticMenuload+6); // Write jump out game.WriteJumpInstruction(staticMenuload, codeMenuload); // Write jump in vars.injected = true; vars.origGametime = codeGametime+7; vars.origMapload = codeMapload+7; vars.origMenu = codeMenuload+7; vars.log("INFO","Finished injecting"); game.Resume(); } vars.isLoading = new MemoryWatcher(aslMem); } isLoading{ return vars.isLoading.Current; } start { vars.lastMap = ""; // TDD run start if(current.map == "RainyHall") return old.audio != current.audio && current.audio == "CH01L00_DanielsMind01_01"; // Justine run start if(current.map == "L01Cells") return current.loading1 == current.loading2 && old.loading1 != current.loading1; } reset { if(current.map == "RainyHall") return current.audio2 == "23_amb.ogg" && old.audio2 == "game_menu.ogg" && current.loading1 == current.loading2; else return current.map != old.map && current.map == "L01Cells"; } update { vars.isLoading.Update(game); if(current.map != vars.lastMap && old.map != null && old.map != "") vars.lastMap = old.map; } split { if(current.map != old.map && (current.map == "RainyHall" || current.map == "L01Cells")) return; //if(current.map != null && current.map != "" && vars.lastMap != current.map) // vars.log("MAP","\""+current.map+"\", was \""+vars.lastMap+"\""); if(current.map == old.map){ // TDD run end if(current.map == "OrbChamber" && old.audio != current.audio) return current.audio == "CH03L29_Alexander_Interrupt03_01"|| // TDD Daniel ending current.audio == "CH03L29_Ending_Alexander_01"|| // TDD Alexander ending current.audio == "CH03L29_Alexander_AgrippaEnd_01"; // TDD Agrippa ending // Justine run end if(current.map == "L04Final"){ if(current.pActive && current.pPosX > 37.62f) return current.pActive && old.pPosX < 37.62f && current.pPosX > 37.62f; return !current.pActive && current.pMSMul == 0.3f && old.pMSMul == 0.4f; } } // Level changes return current.map != null && current.map != "" && vars.lastMap != "" && vars.lastMap != current.map && settings["fullSplit"]; }