state("TheLostCrown") {} state("TheLostCrown_plus") {} startup { Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity"); vars.Helper.GameName = "POPTLC"; vars.Helper.LoadSceneManager = true; // The ubisoft+ version of this game is weird and requires overriding some config in asl-help vars.Helper.Il2CppModules.Add("GameAssembly_plus.dll"); vars.Helper.DataDirectory = "TheLostCrown_Data"; vars.Helper.Settings.CreateFromXml("Components/POPTLC.Settings.xml"); vars.Helper.StartFileLogger("POPTLC Autosplitter.log"); vars.Watch = (Action<IDictionary<string, object>, IDictionary<string, object>, string>)((oldLookup, currentLookup, key) => { var oldValue = oldLookup[key]; var currentValue = currentLookup[key]; if (oldValue != null && currentValue != null && !oldValue.Equals(currentValue)) vars.Log(key + ": " + oldValue + " -> " + currentValue); }); vars.CompletedSplits = new HashSet<string>(); vars.IGTValue = 0; // 0: Not running, 1: Full game, 2: DLC, 3: Divine Trials IL vars.mode = 0; if (timer.CurrentTimingMethod != TimingMethod.GameTime) { DialogResult mbox = MessageBox.Show(timer.Form, "This game uses an in-game timer as the primary timing method.\nWould you like to switch to Game Time?", "LiveSplit | Prince of Persia: The Lost Crown", MessageBoxButtons.YesNo); if (mbox == DialogResult.Yes) { timer.CurrentTimingMethod = TimingMethod.GameTime; } } } init { vars.NORMAL_START_SCENE = "KIN_BAT_02"; vars.DLC_START_SCENE = "RAD_INT_01_BATTLEFIELD"; vars.Helper.TryLoad = (Func<dynamic, bool>)(mono => { // Asl-help has this issue where sometimes offsets resolve to 0x10 less than what they are meant to be, this is a fix to that... var PAD = 0x10; var CBH_STATE_OFFSET = 0x40; var PM = mono["Alkawa.Gameplay", "PauseManager"]; vars.Helper["isPaused"] = PM.Make<bool>("m_paused"); var PC = mono["Alkawa.Gameplay", "PlayerComponent"]; var PISC = mono["Alkawa.Gameplay", "PlayerInputSubComponent"]; var PISI = mono["Alkawa.Gameplay", "PlayerInputStateInfo", 1]; // enum EPlayerInputMode: Gameplay=0, Menu=1, Conversation=2, CutScene=3, Popup=4, NoInput=5, Unknown=-1 vars.Helper["inputMode"] = PM.Make<int>( "m_PlayerComponent", PC["PlayerInput"] + PAD, PISC["m_inputStateInfo"] + PAD, PISI["m_inputMode"] + PAD ); var PASC = mono["Alkawa.Gameplay", "PlayerAbilitiesSubComponent"]; var PASI = mono["Alkawa.Gameplay", "PlayerAbilitiesStateInfo"]; var ABILITY = mono["Alkawa.Gameplay", "Ability"]; vars.Helper["playerAction"] = PM.Make<int>( "m_PlayerComponent", PC["PlayerAbilities"] + PAD, PASC["m_stateInfo"] + PAD, PASI["m_ability"] + PAD, ABILITY["m_currentPlayerActionInternal"] + PAD ); vars.Helper["unlockableAbilities"] = PM.MakeList<IntPtr>( "m_PlayerComponent", PC["PlayerAbilities"] + PAD, PASC["m_stateInfo"] + PAD, PASI["m_unlockableAbilities"] + PAD ); vars.CheckIfAbilityUnlocked = (Func<IntPtr, bool>)(ability => { bool unlocked = vars.Helper.Read<bool>(ability + ABILITY["m_enabled"] + PAD); return unlocked; }); vars.Helper["isClairvoyanceUnlocked"] = PM.Make<bool>( "m_PlayerComponent", PC["PlayerAbilities"] + PAD, PASC["m_stateInfo"] + PAD, PASI["m_isClairvoyanceEnabled"] + PAD ); var LM = mono["Alkawa.Gameplay", "LootManager", 1]; var LI = mono["Alkawa.Engine", "LevelInstance"]; var LD = mono["Alkawa.Engine", "LevelData", 1]; vars.Helper["level"] = LM.MakeString( "m_instance", LM["m_currentLevelInstance"] + PAD, LI["m_levelData"] + PAD, LD["m_prettyName"] + PAD ); vars.Helper["shortLevel"] = LM.MakeString( "m_instance", LM["m_currentLevelInstance"] + PAD, LI["m_levelData"] + PAD, LD["m_shortPrettyName"] + PAD ); // Boss var UIM = mono["Alkawa.Gameplay", "UIManager", 1]; var UI_HP = mono["Alkawa.Gameplay", "UI_HP", 1]; var HSI = mono["Alkawa.Gameplay", "HealthStateInfo"]; var ED = mono["Alkawa.Gameplay", "EntityDescriptor"]; var UISLI = mono["Alkawa.Gameplay", "UISmartLocId"]; vars.Helper["boss1LocId"] = UIM.Make<int>( "m_instance", UIM["m_BossHealthBar"] + PAD, UI_HP["m_entityDescriptor"] + PAD, ED["Name"] + PAD, UISLI["m_locId"] + PAD ); vars.Helper["boss1Health"] = UIM.Make<int>( "m_instance", UIM["m_BossHealthBar"] + PAD, UI_HP["m_healthStateInfo"] + PAD, HSI["m_internalCurrentHP"] + PAD ); vars.Helper["boss1Health"].FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull; vars.Helper["boss2Health"] = UIM.Make<int>( "m_instance", UIM["m_SecondBossHealthBar"] + PAD, UI_HP["m_healthStateInfo"] + PAD, HSI["m_internalCurrentHP"] + PAD ); vars.Helper["boss2Health"].FailAction = MemoryWatcher.ReadFailAction.SetZeroOrNull; var SM = mono["Alkawa.Gameplay", "SpeedrunManager", 1]; var SI = mono["Alkawa.Gameplay", "SpeedrunInstance"]; vars.Helper["speedrunTimer"] = SM.Make<double>( "m_instance", SM["m_mainGameSpeedrunInstance"] + PAD, SI["m_currentTimer"] + PAD ); // Divine Trials var CM = mono["Alkawa.Gameplay", "ChallengeManager", 1]; vars.Helper["challengeType"] = CM.Make<int>( "m_instance", CM["m_currentChallengeType"] + PAD ); vars.Helper["challengeState"] = CM.Make<int>( "m_instance", CM["m_currentHandler"] + PAD, CBH_STATE_OFFSET ); return true; }); // This function is a helper for checking splits that may or may not exist in settings and if we want to do them only once vars.CheckSplit = (Func<string, bool>)(key => { // Make sure splits are enabled and timer is running if (!settings.SplitEnabled || timer.CurrentPhase != TimerPhase.Running) { return false; } // If the split doesn't exist, or it's off, or we've done it already if (!settings.ContainsKey(key) || !settings[key] || !vars.CompletedSplits.Add(key) ) { return false; } vars.Log("Completed: " + key); return true; }); } update { current.activeScene = vars.Helper.Scenes.Active.Name ?? current.activeScene; vars.Watch(old, current, "activeScene"); vars.Watch(old, current, "level"); vars.Watch(old, current, "shortLevel"); vars.Watch(old, current, "inputMode"); vars.Watch(old, current, "boss1LocId"); // vars.Watch(old, current, "boss1Health"); // vars.Watch(old, current, "boss2Health"); // vars.Watch(old, current, "playerAction"); } onStart { timer.IsGameTimePaused = true; // Refresh all splits when we start the run, none are yet completed vars.CompletedSplits.Clear(); vars.Log(settings.SplitEnabled); vars.Log(current.isPaused); vars.Log(current.level); vars.Log(current.inputMode); vars.Log(current.playerAction); vars.Log(current.boss1LocId); vars.Log(current.boss1Health); vars.Log(current.boss2Health); } onReset { vars.IGTValue = 0; vars.mode = 0; } start { if (old.activeScene == "UIManager") return false; if (current.activeScene == vars.NORMAL_START_SCENE && old.activeScene == vars.NORMAL_START_SCENE) { vars.mode = 1; } else if (current.activeScene == vars.DLC_START_SCENE && old.activeScene != vars.DLC_START_SCENE) { vars.mode = 2; } else if (current.challengeType == 6 && old.challengeState == 2 && current.challengeState == 4 && current.speedrunTimer > 0) { vars.mode = 3; } if (vars.mode > 0) { vars.IGTValue = 0; return true; } } reset { // Auto-reset only for Divine Trials return (vars.mode == 3 && current.challengeState != 4 && current.challengeState != 8); } isLoading { return true; } gameTime { if (old.speedrunTimer > 0 && current.speedrunTimer > old.speedrunTimer) { vars.IGTValue += (current.speedrunTimer - old.speedrunTimer); } // IGT doesn't start until cutscene is playing in the opening scene of the respective categories if ((current.activeScene == vars.NORMAL_START_SCENE || (current.activeScene == vars.DLC_START_SCENE && vars.mode == 2)) && current.inputMode == 3) { vars.IGTValue = 0; } return TimeSpan.FromSeconds(vars.IGTValue); } split { if (vars.mode != 3) { if (settings["abilites"]) { for (int index = 0; index < current.unlockableAbilities.Count; index++) { if (vars.CheckIfAbilityUnlocked(current.unlockableAbilities[index]) && vars.CheckSplit("ability__" + index)) { return true; } } if (current.isClairvoyanceUnlocked && vars.CheckSplit("clairvoyance")) { return true; } } if (settings["quest"]) { if (old.shortLevel != current.shortLevel && vars.CheckSplit("inlevel_" + current.shortLevel)) return true; if (current.level == old.level && vars.CheckSplit("level_action__" + current.level + "__" + current.playerAction)) return true; } if (settings["boss"]) { bool bothDead = current.boss1Health <= 0 && current.boss2Health <= 0; bool oneWasAlive = (old.boss1Health > 0 || old.boss2Health > 0); bool playerAlive = current.playerAction != 65 && !old.isPaused; string key = "boss__" + current.boss1LocId + "__" + current.level; if (bothDead && oneWasAlive && playerAlive && vars.CheckSplit(key)) return true; if (old.playerAction != current.playerAction && vars.CheckSplit("player_action__" + old.playerAction)) return true; } } // Divine Trials if (vars.mode == 3 && current.challengeState == 8) { vars.mode = 0; return true; } }