// Created by Driver // Updates: // Gworld (state{} action), Gengine (state{} action), FNamePool (vars.Funcs.setGameVersion()) and executable hashes (vars.Funcs.setGameVersion()) state("LegoHorizonAdventures-Win64-Shipping", "v1.4.0.0-Steam"){ // GWorld, levelFname ulong levelFName : 0x890E738, 0x18; // GWorld, Levels, Levels[1], WorldContainer, sceneFName ulong sceneFName : 0x890E738, 0x170, 0x8, 0x20, 0x18; // GEngine, GameInstance, 0x108, 0x1D0, goldBricks int goldBricks : 0x890B8A8, 0x1088, 0x108, 0x1D0, 0x2A4; // Gold bricks for the coop player: int goldBricksRemote : 0x890B8A8, 0xA58, 0xE8, 0xD00, 0x2B8, 0x8; // Loading flags: // GEngine, GameInstance, 0x108, GlowMusicSubsystem, GlowMusicGameplayHandler, Flag bool isPaused : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1EE; bool isCinematic : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1EF; bool isConversation : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1F1; bool isLoading : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x208; bool isFadeToBlack : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x209; bool isBetweenWorlds : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x20A; bool isWorldTransition : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x20B; } // EpicGames version's memory addresses are the same as the Steam version, as for patch // 1.3, but an independant state is included here in case this changes in the future: state("LegoHorizonAdventures-Win64-Shipping", "v1.4.0.0-EpicGames"){ // GWorld, levelFname ulong levelFName : 0x890E738, 0x18; // GWorld, Levels, Levels[1], WorldContainer, sceneFName ulong sceneFName : 0x890E738, 0x170, 0x8, 0x20, 0x18; // GEngine, GameInstance, 0x108, 0x1D0, goldBricks int goldBricks : 0x890B8A8, 0x1088, 0x108, 0x1D0, 0x2A4; // Gold bricks for the coop player: int goldBricksRemote : 0x890B8A8, 0xA58, 0xE8, 0xD00, 0x2B8, 0x8; // Loading flags: // GEngine, GameInstance, 0x108, GlowMusicSubsystem, GlowMusicGameplayHandler, Flag bool isPaused : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1EE; bool isCinematic : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1EF; bool isConversation : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x1F1; bool isLoading : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x208; bool isFadeToBlack : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x209; bool isBetweenWorlds : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x20A; bool isWorldTransition : 0x890B8A8, 0x1088, 0x108, 0x368, 0x288, 0x20B; } // Script is executed startup{ // Object containing useful functions: vars.Funcs = new ExpandoObject(); // Contains a list of checkpoints and coorelates with the split list: vars.checkpoints = new Dictionary<string, ExpandoObject>(); // Contains de autosplitter settings: dynamic[,] _settings = { // ID prefixes: // SSS: Start on scene start (currently not used) // SSE: Start on scene ending // WS: Split on world start (currently not used) // WE: Split on world ending // SS: Split on scene start (currently not used) // SE: Split on scene ending // GB: Split on gold brick // RB: Split on red brick (currently not used) // ID, Label, Tool tip, Parent ID, Default setting? {"SSE_StarterFrontendMap", "Start", "Starts the autosplitter automatically after starting a playthrough", null, true}, {"m_quests", "Any%", null, null, true}, {"WE_Prologue_P", "Prologue", "After completing the prologue", "m_quests", true}, {"ch1", "Ch. 1- Rescuing the Nora", null, "m_quests", true}, {"GB_B1_Room_003_Lighting_Day", "Into the woods", "", "ch1", true}, {"GB_SR_B1_Tallneck_Exit_Lighting_Morning_Tallneck_Exit", "Head in the clouds", "", "ch1", true}, {"GB_B1_Room_010_Lighting_Day_Forest", "Cult following", "", "ch1", true}, {"GB_B1_Room_006_Lighting_Evening_Forest", "Face to the sun", "", "ch1", true}, {"GB_TreasureRoom_01_Lighting_Evening", "A strange detour", "", "ch1", true}, {"GB_B1_Room_004_Lighting_Night", "A girl and her destiny", "", "ch1", true}, {"ch2", "Ch. 2 - Thunder in the Mountains", null, "m_quests", true}, {"GB_B2_Room_001_Lighting_Day_Mountain", "The cold calling", "", "ch2", true}, {"thaa", "The hills are alive", "None of the following interfiere with one another, so you can leave all of them checked", "ch2", true}, {"GB_B2_TreasureRoom_001_Lighting_Night_Mountain", "The hills are alive", "", "thaa", true}, {"GB_B2_TreasureRoom_001", "The hills are alive [Coop]", "Activate this one for coop playthroughs, but leave the oringinal one activated as well", "thaa", true}, {"GB_B2_TreasureRoom_001_Terrain", "The hills are alive [Coop]", "Activate this one for coop playthroughs, but leave the oringinal one activated as well", "thaa", true}, {"GB_B2_Room_003_Lighting_Day_Mountain", "Frosted peaks", "", "ch2", true}, {"GB_B2_TreasureRoom_001_Lighting_Day_Mountain", "The shivering summit", "", "ch2", true}, {"GB_B2_Room_007_Lighting_Evening_Mountain", "A tower achievement", "", "ch2", true}, {"GB_Cau_Boss_Lighting", "The belly of the beast", "", "ch2", true}, {"ch3", "Ch. 3 - Desperately Seeking Sawtooths", null, "m_quests", true}, {"GB_B3_Room_001_Lighting_Morning_Jungle", "Lair of the tree haters", "", "ch3", true}, {"dus", "Digging up secrets", "", "ch3", true}, {"GB_SR_B3_Tallneck_01_Exit_Lighting_Day_Jungle", "Tallneck route", "", "dus", true}, {"GB_B3_TreasureRoom_001_Lighting_Day_Jungle", "Battle route", "", "dus", true}, {"GB_B3_Room_006_Lighting_Evening_Jungle", "Home cooking", "", "ch3", true}, {"GB_B3_Room_009_Lighting_Night_Jungle", "Legend of the mithyc tale", "", "ch3", true}, {"inr", "Instructions not required", "None of the following interfiere with one another, so you can leave all of them checked", "ch3", true}, {"GB_B3_Boss_Room_BiomassFacility_Lighting", "Instructions not required", "On gold brick, single player", "inr", true}, {"GB_B3_Boss_Room_BiomassFacility", "Instructions not required [Coop]", "Activate this one for coop playthroughs, but leave the oringinal one activated as well", "inr", true}, {"GB_B3_Boss_Room_BiomassFacility_Terrain", "Instructions not required [Coop]", "Activate this one for coop playthroughs, but leave the oringinal one activated as well", "inr", true}, {"ch4", "Ch. 4 - Drawing Out Helis", null, "m_quests", true}, {"GB_B4_Room_002_Lighting_Day_Desert", "The desert flower", "", "ch4", true}, {"GB_B4_Room_004_Lighting_Day_Desert", "Flavors of a lost world", "", "ch4", true}, {"mato", "Midday at the oasis", "", "ch4", true}, {"GB_SR_B4_Tallneck_01_Exit_Lighting_Day_Desert", "Tallneck route", "", "mato", true}, {"GB_B4_TreasureRoom_001_Lighting_Day_Desert", "Battle route", "", "mato", true}, {"GB_B4_Room_007_Lighting_Night_Desert", "We can be heroes", "", "ch4", true}, {"GB_B4_Room_005_Lighting_Evening_Desert", "Sundown showdown", "", "ch4", true}, {"SE_B5_Room_003_Lighting_Destruction_Jungle", "Pre-Hades", "Right before entering the final battle, before the loading screen.", "ch4", false}, {"GB_B5_Boss_Room_001_Lighting_Destruction_Jungle_Hell", "The final battle", "", "ch4", true} }; // Calculates the hash of a given module. // Taken from ISO2768mK's Horizon Forbidden West load remover: vars.Funcs.hashModule = (Func<ProcessModuleWow64Safe, string>)((module) => { byte[] hashBytes = new byte[0]; using (var sha256Object = System.Security.Cryptography.SHA256.Create()) { using (var binary = File.Open(module.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { hashBytes = sha256Object.ComputeHash(binary); } } var hexHashString = hashBytes.Select(x => x.ToString("X2")).Aggregate((a, b) => a + b); return hexHashString; }); // Default variables (Patch 1.4.0.0 Steam version): version = "v1.4.0.0-Steam"; vars.FNamePoolOffset = 0x86EBDC0; // Determines the running game version and initialize variables acordingly: // Patch 1.4.0.0 EpicGames: vars.Funcs.setGameVersion = (Action<string>)((hash) => { if(hash == "5EF34C4A1D75419D344F1C6B24C5354E02CB8E633776C95FEC608151410A2C1F"){ version = "v1.3.0.0-EpicGames"; vars.FNamePoolOffset = 0x86EBDC0; print("Detected game version: " + version); // Patch 1.4.0.0 Steam: }else if(hash == "7344F667E834475C75A0A03FA238E6FC5FE979780FFE755340B46E80F2E4BCCA"){ // Don't do anything, the default variables are the Steam ones... print("Detected game version: " + version); } else{ // If no version was identified, show a warning message: MessageBox.Show( "The Autosplitter could not identify the game version, the default version was set to " + version + ".\nIf this is not the version of your game, the Autosplitter might not work properly.", "LHA Autosplitter", MessageBoxButtons.OK, MessageBoxIcon.Warning ); } }); // Initialize the autosplitter settings dynamic tmp = null; for (int i = 0; i < _settings.GetLength(0); i++){ tmp = new ExpandoObject(); tmp.reachedBefore = false; // Autosplitter settings entry: // settings.Add(id, default_value = true, description = null, parent = null) settings.Add(_settings[i, 0], _settings[i, 4], _settings[i, 1], _settings[i, 3]); // Tool tip message (if available) if(_settings[i, 2] != null){ settings.SetToolTip(_settings[i, 0], _settings[i, 2]); } vars.checkpoints.Add(_settings[i, 0], tmp); } } // startup ends init{ // Gets the running game module hash: var moduleHash = vars.Funcs.hashModule(modules.First()); print(moduleHash); // Initialize variables acording to the running version: vars.Funcs.setGameVersion(moduleHash); // Determines if the game is currently loading: vars.Funcs.isLoading = (Func<bool>)(() => { // Comment out unwanted flags: return // current.isPaused || // Pause menu (the one when you press ESC) // current.isCinematic || // Cinematics // current.isConversation || // Conversations current.isLoading || // General game loading // current.isFadeToBlack || // That brief moment when the screen fades in or out current.isBetweenWorlds || // Scene and world transitions current.isWorldTransition; // World transitions // Title screen: // vars.Funcs.FNameToString(current.levelFName) == "StarterFrontendMap"; }); // Determines if it is time to split: vars.Funcs.isSplit = (Func<string, bool>)((splitName) => { if( // Splits only if the user has enabled this split in the autosplitter settings: settings[splitName] && // Splits only once per checkpoint: !vars.checkpoints[splitName].reachedBefore ){ vars.checkpoints[splitName].reachedBefore = true; print("SPLIT: " + splitName); return true; } else{ return false; } }); // Unreal Engine's structures ******************************************** IntPtr FNamePool = modules.First().BaseAddress + vars.FNamePoolOffset; // *********************************************************************** // Takes an FName object and returns the asociated string. // Based on TheDementedSalad's Silent Hill 2 remake autosplitter: vars.Funcs.FNameToString = (Func<ulong, string>)(fName => { var nameIdx = (fName & 0x000000000000FFFF) >> 0x00; var chunkIdx = (fName & 0x00000000FFFF0000) >> 0x10; IntPtr chunk = memory.ReadValue<IntPtr>(FNamePool + 0x10 + (int)chunkIdx * 0x8); IntPtr entry = chunk + (int)nameIdx * sizeof(short); int length = memory.ReadValue<short>(entry) >> 6; string name = memory.ReadString(entry + sizeof(short), length); return name; }); } // init ends update{ // print(vars.Funcs.FNameToString(current.levelFName)); // print(vars.Funcs.FNameToString(current.sceneFName)); // print(current.goldBricksRemote.ToString()); } isLoading{ return vars.Funcs.isLoading(); } start{ if(old.levelFName != current.levelFName){ var currentWorldName = vars.Funcs.FNameToString(current.levelFName); var oldWorldName = vars.Funcs.FNameToString(old.levelFName); print("\n\nWorld changed: " + currentWorldName); // World ending starting points: if(vars.Funcs.isSplit("SSE_" + oldWorldName)){ return true; } } } split{ // Checks if the timer is actually running: if (timer.CurrentPhase == TimerPhase.Running){ var currentSceneName = vars.Funcs.FNameToString(current.sceneFName); // Gold brick splits: if(current.goldBricks > old.goldBricks || current.goldBricksRemote != old.goldBricksRemote){ print("\n\nGold brick collected."); print("Local gold bricks: " + current.goldBricks.ToString() + " | " + "Remote gold bricks: " + current.goldBricksRemote.ToString()); if(vars.Funcs.isSplit("GB_" + currentSceneName)){ return true; } } // World splits: if(old.levelFName != current.levelFName){ var currentWorldName = vars.Funcs.FNameToString(current.levelFName); var oldWorldName = vars.Funcs.FNameToString(old.levelFName); print("\n\nWorld changed: " + currentWorldName); // World ending splits: if(vars.Funcs.isSplit("WE_" + oldWorldName)){ return true; } // World start splits: if(vars.Funcs.isSplit("WS_" + currentWorldName)){ return true; } } // Scene splits: if(old.sceneFName != current.sceneFName){ var oldSceneName = vars.Funcs.FNameToString(old.sceneFName); print("\n\nScene changed: " + currentSceneName); // Scene ending splits: if(vars.Funcs.isSplit("SE_" + oldSceneName)){ return true; } // Scene start splits: if(vars.Funcs.isSplit("SS_" + currentSceneName)){ return true; } } } } onReset{ foreach (var checkpoint in vars.checkpoints){ checkpoint.Value.reachedBefore = false; } } // Clean up to pre-init state when game process is closed exit { foreach (var checkpoint in vars.checkpoints){ checkpoint.Value.reachedBefore = false; } } // exit END