diff --git a/projects/Boneworks/SpeedrunTools/src/Features/ArenaSplitState.cs b/projects/Boneworks/SpeedrunTools/src/Features/ArenaSplitState.cs new file mode 100644 index 0000000..aa93f3d --- /dev/null +++ b/projects/Boneworks/SpeedrunTools/src/Features/ArenaSplitState.cs @@ -0,0 +1,50 @@ +using MelonLoader; +using HarmonyLib; +using UnityEngine; +using StressLevelZero.Arena; + +namespace Sst.Features { +class ArenaSplitState : Feature { + // State that the ASL finds using signature scanning + private static byte[] State = { + // 0 = magic string start + // Signature is set dynamically to avoid finding this hardcoded array + 0x00, // 0xD5 + 0xE2, + 0x03, + 0x34, + 0xC2, + 0xDF, + 0x63, + // 7 = arena index + 0x00, + }; + + private static bool _isCurrentChallengeCompleted = true; + + public ArenaSplitState() { State[0] = 0xD5; } + + [HarmonyPatch(typeof(Arena_GameManager), + nameof(Arena_GameManager.RingTheBell))] + class ArenaGameManager_RingTheBell_Patch { + [HarmonyPostfix()] + internal static void Postfix(Arena_GameManager __instance) { + _isCurrentChallengeCompleted = + __instance.activeChallenge?.profile?.completedHard ?? true; + } + } + + [HarmonyPatch(typeof(Arena_GameManager), + nameof(Arena_GameManager.SaveChallengeCompletion))] + class ArenaGameManager_SaveChallengeCompletion_Patch { + [HarmonyPostfix()] + internal static void Postfix(Arena_GameManager __instance) { + if (!_isCurrentChallengeCompleted && + __instance.activeChallenge.profile.completedHard) { + _isCurrentChallengeCompleted = true; + State[7]++; + } + } + } +} +} diff --git a/projects/Boneworks/SpeedrunTools/src/Mod.cs b/projects/Boneworks/SpeedrunTools/src/Mod.cs index 218c7e2..b7a11ed 100644 --- a/projects/Boneworks/SpeedrunTools/src/Mod.cs +++ b/projects/Boneworks/SpeedrunTools/src/Mod.cs @@ -31,6 +31,7 @@ public class Mod : MelonMod { new Features.Gripless(), new Features.Armless(), new Features.RestartLevel(), + new Features.ArenaSplitState(), // Dev features new Features.Speedometer(), diff --git a/scripts/Boneworks100.asl b/scripts/Boneworks100.asl new file mode 100644 index 0000000..6ca120e --- /dev/null +++ b/scripts/Boneworks100.asl @@ -0,0 +1,110 @@ +state("BONEWORKS") { + int levelNumber : "GameAssembly.dll", 0x01E7E4E0, 0xB8, 0x590; +} + +init { + var module = modules.First(x => x.ModuleName == "vrclient_x64.dll"); + + var scanner = + new SignatureScanner(game, module.BaseAddress, module.ModuleMemorySize); + + vars.loadingPointer = scanner.Scan(new SigScanTarget( + 3, + "4889??????????488B??????????48FF????488B??????????488B??4889??????????488D??????????488B??????????48FF??????????????????????????????488B??????????418B") { + OnFound = (process, scanners, addr) => + addr + 0x4 + process.ReadValue(addr) + }); + if (vars.loadingPointer == IntPtr.Zero) { + throw new Exception("Game engine not initialized - retrying"); + } + + vars.isLoading = + new MemoryWatcher(new DeepPointer(vars.loadingPointer, 0xC54)); + + var arenaTarget = new SigScanTarget(7, "D5 E2 03 34 C2 DF 63 ??"); + IntPtr ptr = IntPtr.Zero; + foreach (var page in game.MemoryPages(true)) { + var pageScanner = + new SignatureScanner(game, page.BaseAddress, (int)page.RegionSize); + ptr = pageScanner.Scan(arenaTarget); + if (ptr != IntPtr.Zero) + break; + } + if (ptr == IntPtr.Zero) { + throw new Exception("Arena state not found - retrying"); + } + vars.arenaWatcher = new MemoryWatcher(ptr); + + // Index in levelOrder to start the run at (for practice) + vars.startingSplit = 0; + + // Will split when entering each level in this list in this order + // If entering a level later in the list it will split until it reaches it + vars.levelOrder = new int[] { + 2, // scene_theatrigon_movie01 -> scene_breakroom + 4, // scene_museum + 5, // scene_streets + 5, // scene_streets + 6, // scene_runoff + 6, // scene_runoff + 7, // scene_sewerStation + 8, // scene_warehouse + 9, // scene_subwayStation + 10, // scene_tower + 11, // scene_towerBoss + 12, // scene_theatrigon_movie02 -> scene_dungeon + 14, // scene_arena + 15, // scene_throneRoom + 1, // scene_mainMenu + 18, // scene_redactedChamber + 19, // sandbox_handgunBox + 22, // scene_hoverJunkers + 16, // arena_fantasy + 23, // zombie_warehouse + // 23, // zombie_warehouse + // 23, // zombie_warehouse + 1, // scene_mainMenu + }; + vars.levelOrderIdx = 0; + vars.targetLevelOrderIdx = vars.startingSplit; +} + +update { + vars.isLoading.Update(game); + vars.arenaWatcher.Update(game); +} + +isLoading { return vars.isLoading.Current; } + +start { + if (vars.isLoading.Current && + current.levelNumber == vars.levelOrder[vars.startingSplit]) { + vars.levelOrderIdx = 0; + return true; + } + return false; +} + +split { + if (vars.arenaWatcher.Current != vars.arenaWatcher.Old) { + return true; + } + + if (vars.isLoading.Current && + (!vars.isLoading.Old || current.levelNumber != old.levelNumber)) { + var nextLevelOrderIdx = vars.levelOrderIdx + 1; + if (nextLevelOrderIdx < vars.levelOrder.Length && + vars.levelOrder[nextLevelOrderIdx] == current.levelNumber) { + vars.targetLevelOrderIdx = nextLevelOrderIdx; + } + } + + if (vars.levelOrderIdx < vars.targetLevelOrderIdx) { + vars.levelOrderIdx++; + return true; + } + + return false; +} + +exit { timer.IsGameTimePaused = true; }