state("Project64", "PJ64") { int level1LoadScreen : "Project64.exe", 0x107AD8, 0x3480; byte fBoss1Health : "Project64.exe", 0x107AD8, 0x2120; byte fBoss2Health : "Project64.exe", 0x107AD8, 0x2124; byte fBoss3Health : "Project64.exe", 0x107AD8, 0x2128; short buzzXPos : "Project64.exe", 0x107AD8, 0xBB078; byte16 tokenCount : "Project64.exe", 0x107AD8, 0x1E2DD0; int inLevel : "Project64.exe", 0x107AD8, 0xB0B58; } state("toy2", "US") { int isLoadScreen : "toy2.exe", 0x15A0E0; int levelID : "toy2.exe", 0x15A0E4; byte fBoss1Health : "toy2.exe", 0x130028; byte fBoss2Health : "toy2.exe", 0x13002C; byte fBoss3Health : "toy2.exe", 0x130030; short buzzXPos : "toy2.exe", 0x12F308; byte16 tokenCount : "toy2.exe", 0x12F0D8; int inLevel : "toy2.exe", 0x13EEEC; } state("toy2", "UK") { int isLoadScreen : "toy2.exe", 0x15B2E0; int levelID : "toy2.exe", 0x15B2E4; byte fBoss1Health : "toy2.exe", 0x1311A8; byte fBoss2Health : "toy2.exe", 0x1311AC; byte fBoss3Health : "toy2.exe", 0x1311B0; short buzzXPos : "toy2.exe", 0x130488; byte16 tokenCount : "toy2.exe", 0x130258; int inLevel : "toy2.exe", 0x14006C; } startup { settings.Add("final_boss_split", true, "Split when defeating the final boss"); settings.Add("level_split", true, "Split by levels instead of tokens"); } init { if (game.ProcessName == "Project64") { version = "PJ64"; } else { var baseAddr = modules.First().BaseAddress; var isLoadingAddr = IntPtr.Zero; // detect version by using the date modified value of the EXE (the US is 0 because the header starts later) vars.modifiedDate = memory.ReadValue(baseAddr + 0xF8); if (vars.modifiedDate == 0) { version = "US"; isLoadingAddr = baseAddr + 0xA4EFFC; } else if (vars.modifiedDate == 0x388395FF) { version = "UK"; isLoadingAddr = baseAddr + 0xA51FFC; } else { version = ""; } // Inject load detection code var hooks = new dynamic[,] { { 0, "83 EC 0C 8B 44 24 14 53" , 7, false }, // 0x414320 Enter PressJump_LevelOrFmv { 0, "8B 44 24 08 83 C4 0C" , 7, true }, // 0x414546 Leave PressJump_LevelOrFmv { 0, "53 55 56 33 DB 57 66" , 5, true }, // 0x414720 Enter LoadLevel { 4, "83 C4 04 5F 8B C5" , 5, false }, // 0x414A70 Leave LoadLevel { 0, "A1 ?? ?? ?? ?? 81 EC 10 01 00 00 53 55 56", 5, true }, // 0x452FC0 Enter InitLevelPlay { 0, "81 C4 10 01 00 00 C3 90 90 C3" , 6, false }, // 0x453C67 Leave InitLevelPlay { 0, "8B 7C 24 10 85 FF 74 0A" , 6, true }, // 0x49AB99 Enter PlayFmv { 0, "83 C4 04 5F 5D C3" , 5, false }, // 0x49AC26 Leave PlayFmv 1 { 0, "8B C5 5F 5D" , 5, false }, // 0x49AC2C Leave PlayFmv 2 { 0, "8B 2D ?? ?? ?? ?? 83 C4 24" , 6, false }, // 0x4CE634 PlayFmvByFilename: start playback { 0, "83 C4 04 8B C3 5F" , 5, true } // 0x4CE6A7 PlayFmvByFilename: stop playback }; var scanner = new SignatureScanner(game, new IntPtr(0x401000), 0xDB000); for (int i = 0; i < hooks.GetLength(0); ++i) { var signatureOffset = hooks[i, 0]; var signature = hooks[i, 1]; int overwrittenBytes = hooks[i, 2]; var increment = hooks[i, 3]; // Find address var hookAddr = scanner.Scan(new SigScanTarget(signatureOffset, signature)); if (hookAddr == IntPtr.Zero) { print("Cannot find address to hook."); break; } /* inc/dec jmp */ var code = new byte[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00 }; // Allocate memory for code var codeAddr = memory.AllocateMemory(code.Length); if (codeAddr == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(); // Adjust code and inject it code[1] = (byte)(increment ? 0x05 : 0x0D); BitConverter.GetBytes(isLoadingAddr.ToInt32()).CopyTo(code, 2); var gateAddr = memory.WriteDetour(hookAddr, overwrittenBytes, codeAddr); BitConverter.GetBytes(gateAddr.ToInt32() - codeAddr.ToInt32() - 11).CopyTo(code, 7); if (!memory.WriteBytes(codeAddr, code)) throw new System.ComponentModel.Win32Exception(); } vars.isLoading = new MemoryWatcher(isLoadingAddr); vars.isLoading.Current = 0; vars.isLoading.Enabled = (isLoadingAddr != IntPtr.Zero); } vars.checkBossHealth = false; } update { if (version == "") return false; if (version != "PJ64") { // Only start checking for final boss health if it is the final level, the first boss is at max health, and buzz is in the starting position // This makes it so we will only start checking for final boss health when entering the final level if (current.levelID == 15 && current.fBoss1Health == 29 && current.buzzXPos == -5416) vars.checkBossHealth = true; if (current.levelID != 15) vars.checkBossHealth = false; // Make sure we aren't checking for final boss health if it isn't the final level vars.isLoading.Update(game); } } start { //31815 is the starting position in level 1. This will make it so that once buzz moves at the start of level 1, timer starts return (old.buzzXPos == 31815 && current.buzzXPos != old.buzzXPos); } split { if (settings["final_boss_split"]) { // If enabled, split when final boss is defeated if (version == "PJ64") { // With PJ64, levelID is not supported, so it will continually split after boss is defeated if (current.fBoss1Health==9 && current.fBoss2Health==9 && current.fBoss3Health<=9) return true; } else { // With PC version, we will only split once, after the boss is defeated if (vars.checkBossHealth && current.fBoss1Health==9 && current.fBoss2Health==9 && current.fBoss3Health<=9) { vars.checkBossHealth = false; return true; } } } if (settings["level_split"]) { return (old.inLevel != current.inLevel) && (current.inLevel == 0); } else { return !(Enumerable.SequenceEqual(old.tokenCount, current.tokenCount)); } } reset { if (version == "PJ64") return current.level1LoadScreen == 0; else return (current.levelID == 1 && current.isLoadScreen == 1); } isLoading { if (version == "PJ64") return false; // Not supported for N64 else return vars.isLoading.Current != 0; }