Last active
February 9, 2024 16:13
-
-
Save Alecat/084ba4995b5cfa86021d2ce841717ba5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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<float>(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; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Known issues: after the introduction of challenges, some levels will not have live times tracked during splits. The splitter should still produce the correct sum of splits after pressing the "Done" prompt at the end of a stage.
Sometimes, a wrong time is registered for a level. The sum of segments will fix itself after the end of the next level, but you may wish to manually update the split times as they might not represent realistic values.