/* * KeyWe AutoSplitter for LiveSplit * * Designed to work from a save file starting from level 0 * Splits on completion of every level. * * Start: When the level is selected from the calendar * Split: When the DONE button is pressed on the level-end screen * Reset: When a different save file is loaded (including deleting a save file) * * Pauses the in game timer if any of the following conditions are met: * - after a level ends * - during the loading screen that appears after selecting a level from the calendar * - if the in-level timer is not running * - when no level is selected * * The in-game timer is resumed when a level is selected and has loaded. * The in-game timer is recalculated off sum-of-levels when splitting - they may appear to rewind at split time to cut out times the timer was incorrectly running. * * Issues: * - segment times may round incorrectly? Uncertain what method the in-game timer uses to round - seemed to be truncation in some cases, but not in others... * - does not count time spent in a level that is restarted * * TODO * Options to choose when splitting happens * Can we tell if we've finished a level with a better check than old.currentLevelIndex < current.currentLevelIndex ? (Allows for timing non-fresh runs) */ /* Finding the pointers * In CheatEngine, classes can be found in the Mono Dissector under Assembly-CSharp * Search for the GameFlowController class and check the offset of the dataKeeper field (Currently: 0x28) * Search for the DataKeeper class and check for the offset of the profile field (Currently: 0x68) * Scan for instances of the DataKeeper class * * Determine which of the objects is the most plausible DataKeeper in use. A good way to do this is to observe the currentState value: * currentState enum values * 1 - Not in a level * 2 - In a timed level (including loading the level and viewing its results. Stays in this state if choosing "Restart" on results screen) * 3 - In an untimed level (eg overtime shift) * 4 - Playing a cutscene * * In the pointer search, search for the pointer of the DataKeeper. * Filtering by the last pointer being the offset from the GameFlowController class can be useful * Repeat this process, filtering the results of the pointer scanner after restarting the game multiple times */ state("KeyWe") { // ProfileData ushort currentLevelIndex : "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x68, 0x62; ushort playthroughId : "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x68, 0x66; // int levelRecords: "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x68, 0x10; // LevelLoader bool isLevelLoading: "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x38, 0x28, 0xab; bool isFromRestart: "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x38, 0x28, 0xac; // isWaitingForPlayer may be useful for online games? Appears unused in local play // bool isWaitingForPlayer: "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x38, 0x28, 0xa9; // DataKeeper ushort currentState : "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x98; ushort activeLevelIndex : "UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x8e; // ModeFeedback =20> ModeTimer =38> ElapsedTime float activeLevelTime : "UnityPlayer.dll", 0x017CB888, 0x10, 0x100, 0x30, 0x10, 0xD0, 0x60, 0x20, 0x38; // alternative pointer path // float activeLevelTime : "UnityPlayer.dll", 0x017CB888, 0x18, 0x100, 0x30, 0x10, 0xD0, 0x60, 0x20, 0x38; // Other possible paths...? // ...? GameMode =78> ModeTimer =38> ElapsedTime // ...? Timer => ModeTimer => ElapsedTime } startup { /* TODO: settings */ settings.Add("start_anylevel", false, "Start on any level"); settings.Add("reset_change_savefile", true, "Experimental: Reset on save file change"); settings.Add("track_ingame_loads", true, "Experimental: pause IGT when not in level"); } init { timer.IsGameTimePaused = true; vars.isInMenu = current.currentState != 2; vars.hasSplit = false; vars.gameTime = 0.0; vars.extraAttemptsTime = 0.0; } exit { // Pause the timer if the game is exited vars.isInMenu = true; // Track whether a level was just completed vars.levelCompleted = false; } update { if (current.currentLevelIndex == null) return false; // On the split screen if (old.currentLevelIndex < current.currentLevelIndex) { vars.isInMenu = true; } // Going from calendar to level if (old.currentState == 1 && current.currentState == 2) { vars.isInMenu = false; } } start { if (old.currentState == 1 && current.currentState == 2) { if (settings["start_anylevel"]) { return true; } // Only start on level 0 return current.activeLevelIndex == 0; } } isLoading { if (!settings["track_ingame_loads"]) { return false; } // Pause timer when there's no active level if (current.currentState != 2) { return true; } if (current.isLevelLoading) { return true; } return true; } gameTime { // update game time on split if (old.currentLevelIndex < current.currentLevelIndex || old.playthroughId != current.playthroughId ) { // Recalculate all the times for all levels to be sure vars.gameTime = 0.0; for (int i = 0; i < 36; i++) { vars.levelTime = new DeepPointer("UnityPlayer.dll", 0x0180E7F8, 0x128, 0x88, 0x30, 0x28, 0x28, 0x68, 0x10, 0x20 + i * 0x8, 0x2C).Deref(game); if (vars.levelTime != -1) { // Truncate the level time to 2 decimal places (the value displayed in the UI) // vars.levelTime = Math.Round(vars.levelTime, 2, MidpointRounding.AwayFromZero); vars.levelTime = Math.Truncate(vars.levelTime * 100) / 100; vars.gameTime = vars.gameTime + vars.levelTime; } } return TimeSpan.FromSeconds(vars.gameTime); } else if (!vars.isInMenu && current.currentState == 2 && current.activeLevelTime > 0.1) { return TimeSpan.FromSeconds(vars.gameTime + current.activeLevelTime); } return TimeSpan.FromSeconds(vars.gameTime); } reset { if (settings["reset_change_savefile"] && old.playthroughId != current.playthroughId) { return true; } } split { if (old.currentLevelIndex < current.currentLevelIndex) { vars.hasSplit = false; return false; } // Split when the Done button is pressed if (!vars.hasSplit && old.currentState == 2 && (current.currentState == 1 || current.currentState == 4)) { vars.hasSplit = true; return true; } }