state("Croc2", "US") { int CrocX : 0xA8C3C, 0x14, 0x28, 0x2c; int CrocY : 0xA8C3C, 0x14, 0x28, 0x30; int CrocZ : 0xA8C3C, 0x14, 0x28, 0x34; int CurTribe : 0xA8C44; int CurLevel : 0xA8C48; int CurMap : 0xA8C4C; int CurType : 0xA8C50; int InGameState : 0xB7880; int IsCheatMenuOpen : 0xB788C; bool IsMapLoaded : 0xB78C4; int NewMainState : 0xB7930; int IsNewMainStateValid : 0xB7934; int MainState : 0xB793C; int Inputs : 0x12A590; int GemCounter : 0x12AECC; int GobboCounter : 0x12AEE0; int AllowReturnToHub : 0x12AEFC; int CurSaveSlotIdx : 0x2220FC; int DFCrystal5IP : 0x223D10; } state("Croc2", "EU") { int CrocX : 0xA9C3C, 0x14, 0x28, 0x2c; int CrocY : 0xA9C3C, 0x14, 0x28, 0x30; int CrocZ : 0xA9C3C, 0x14, 0x28, 0x34; int CurTribe : 0xA9C44; int CurLevel : 0xA9C48; int CurMap : 0xA9C4C; int CurType : 0xA9C50; int InGameState : 0xBEA70; int IsCheatMenuOpen : 0xBEA7C; bool IsMapLoaded : 0xBEAB4; int NewMainState : 0xBEB20; int IsNewMainStateValid : 0xBEB24; int MainState : 0xBEB2C; int Inputs : 0x131780; int GemCounter : 0x1320BC; int GobboCounter : 0x1320D0; int AllowReturnToHub : 0x1320EC; int CurSaveSlotIdx : 0x2292EC; int DFCrystal5IP : 0x22AF00; } startup { // Start settings.Add("RequireUnusedBossWarps", true, "Do not start if any boss warp has already been used"); settings.Add("SaveSlotStart", true, "Save slot start"); settings.SetToolTip("SaveSlotStart", "Starts timer on creating a new save file"); settings.Add("InputStart", false, "Start on any input"); settings.SetToolTip("InputStart", "Useful for testing / timing things"); settings.Add("ILstart", false, "IL start"); settings.SetToolTip("ILstart", "Starts timer on map change when you enter a level"); settings.Add("OTSstart", false, "OTS start"); settings.SetToolTip("OTSstart", "Starts timer on map change when you enter hub" + "\n\n(note: used to record times for the Overworld Times Sheet)" + "\n(note: secret is currently disabled, as its OTS isn't set up yet)"); settings.Add("OTSstart_SMP", false, "Also start on SMP entry", "OTSstart"); settings.Add("IWstart", false, "IW start"); settings.SetToolTip("IWstart", "Starts timer on map change when you exit SMP to hub" + "\n\n(note: using the cheat menu to warp is equivalent)" + "\n(fun fact: WW, SQ, GOA, (and sometimes DA) are not equivalent;" + "\nthey give a different hub spawn!)"); // Split settings.Add("SplitOnMapChange", true, "IL/OTS end split"); settings.SetToolTip("SplitOnMapChange", "Splits timer on any relevant map change to end an IL or OTS run" + "\n\nThis can also be used in fullgame runs, to chain multiple IL and OTS segments together!" + "\n(\"IL style splits\")"); settings.Add("SplitOnMapChange_literal", false, "Split on literally any map change", "SplitOnMapChange"); settings.Add("SplitOnObjectiveCompletion", false, "Split on objective completion"); settings.SetToolTip("SplitOnObjectiveCompletion", "(note: you probably want to disable this if doing IL style splits)"); settings.Add("SplitOnSMPEntry", false, "IW end split", "SplitOnObjectiveCompletion"); settings.SetToolTip("SplitOnSMPEntry", "Splits timer on map change into SMP" + "\n\n(note: this option is redundant if IL/OTS end split is enabled!)"); settings.Add("SplitOnGoldenGobbo", false, "100% (require Golden Gobbo)", "SplitOnObjectiveCompletion"); settings.Add("SplitOnDanteCrystals", false, "Split on collecting crystals in Dante's World", "SplitOnObjectiveCompletion"); settings.Add("SplitOnGem", false, "Split on collecting gems"); settings.SetToolTip("SplitOnGem", "(note: useful for IL runs)"); settings.Add("BabiesSubsplits", false, "Babies Subsplits"); for (int i = 1; i <= 30; i++) { settings.Add("BabiesSubsplits_" + i.ToString(), false, i.ToString() + " / 30", "BabiesSubsplits"); } // Debug settings.Add("DebugOutput", false, "Debug output"); settings.SetToolTip("DebugOutput", "Prints debug info in Dbgview.exe" + "\n\n(note: allows you to see which changes happened in the same ASL cycle)"); settings.Add("DO_MapChanges", true, "Map changes", "DebugOutput"); settings.Add("DO_WadB4GH", true, "WadB4GH changes", "DebugOutput"); settings.Add("DO_PrevTribeSSX", true, "PrevTribeSSX changes", "DebugOutput"); settings.Add("DO_MainState", true, "MainState changes", "DebugOutput"); settings.Add("DO_InGameState", true, "InGameState changes", "DebugOutput"); settings.Add("DO_IsCheatMenuOpen", true, "IsCheatMenuOpen changes", "DebugOutput"); settings.Add("DO_AllowReturnToHub", true, "AllowReturnToHub changes", "DebugOutput"); settings.Add("DO_IsMapLoaded", true, "IsMapLoaded changes", "DebugOutput"); // Speed display // should probably move this to a Cheat Engine GUI or a separate asl script or something // https://wiki.cheatengine.org/index.php?title=Tutorial:LuaFormGUI // SetTextComponent is taken from: https://github.com/zment4/DefyGravityASL vars.SetTextComponent = (Action<string, string, bool>)((id, text, create) => { dynamic textSetting = timer.Layout.Components .Where(x => x.GetType().Name == "TextComponent") .Select(x => (x as dynamic).Settings) .FirstOrDefault(x => (x as dynamic).Text1 == id); if (textSetting == null && create) { var textComponentAssembly = Assembly.LoadFrom("Components\\LiveSplit.Text.dll"); dynamic textComponent = Activator.CreateInstance(textComponentAssembly.GetType("LiveSplit.UI.Components.TextComponent"), timer); timer.Layout.LayoutComponents.Add(new LiveSplit.UI.Components.LayoutComponent("LiveSplit.Text.dll", textComponent as LiveSplit.UI.Components.IComponent)); textSetting = textComponent.Settings; textSetting.Text1 = id; } if (textSetting != null) textSetting.Text2 = text; }); settings.Add("SpeedDisplay", false, "Display Croc's speed in a LiveSplit text component"); settings.SetToolTip("SpeedDisplay", "note 1: The text component is created automatically once Croc starts walking around" + "\n\nnote 2: Croc 2 runs at 30fps. This display catches most but NOT ALL of the frames" + "\n\nnote 3: Unfortunately, when Croc isn't moving the display just holds the last non-zero value"); // Returns true iff the current map ID changed vars.HasMapIDChanged = new Func<dynamic, dynamic, bool>((state1, state2) => state1.CurTribe != state2.CurTribe || state1.CurLevel != state2.CurLevel || state1.CurMap != state2.CurMap || state1.CurType != state2.CurType); // Returns true iff map has specified map ID vars.IsThisMap = new Func<dynamic, int, int, int, int, bool>( (state, tribe, level, map, type) => state.CurTribe == tribe && state.CurLevel == level && state.CurMap == map && state.CurType == type); // Returns true iff map is a gobbo hub map vars.IsGobboHub = new Func<dynamic, bool>(state => state.CurTribe >= 1 && state.CurTribe <= 4 && state.CurLevel == 1 && state.CurMap == 1 && state.CurType == 0); // Returns true iff map is "Swap Meet Pete's General Store" vars.IsShopMap = new Func<dynamic, bool>(state => state.CurTribe >= 1 && state.CurTribe <= 4 && state.CurLevel == 1 && state.CurMap == 4 && state.CurType == 0); // Returns true iff a wrong warp is being performed vars.IsWrongWarp = new Func<dynamic, dynamic, bool>((oldState, curState) => // old map is not hub map - can't use functions in functions :( !(oldState.CurTribe >= 1 && oldState.CurTribe <= 4 && oldState.CurLevel == 1 && oldState.CurMap == 1 && oldState.CurType == 0) && // new map is hub map curState.CurTribe >= 1 && curState.CurTribe <= 4 && curState.CurLevel == 1 && curState.CurMap == 1 && curState.CurType == 0 && // return to hub option enabled (usually set to 0 on door, but will stick if ww) curState.AllowReturnToHub == 1 && // saveslot PrevTribe is already 0 (this is the main indicator) // (in fact, this might be what actually causes the wrong warp!) curState.PrevTribeSSX == 0); } init { var firstModule = modules.First(); var baseAddr = firstModule.BaseAddress; int addrScriptMgr; switch (firstModule.ModuleMemorySize) { case 0x23A000: version = "US"; addrScriptMgr = 0xB78BC; vars.AddrCurTribe = baseAddr + 0xA8C44; vars.AddrCurLevel = baseAddr + 0xA8C48; vars.AddrCurMap = baseAddr + 0xA8C4C; vars.AddrCurType = baseAddr + 0xA8C50; vars.AddrSaveSlots = baseAddr + 0x2040C0; vars.AddrUsedBossWarps = baseAddr + 0x222D50; vars.DFCrystal5FinalIP = 0x1741C8; break; case 0x242000: version = "EU"; addrScriptMgr = 0xBEAAC; vars.AddrCurTribe = baseAddr + 0xA9C44; vars.AddrCurLevel = baseAddr + 0xA9C48; vars.AddrCurMap = baseAddr + 0xA9C4C; vars.AddrCurType = baseAddr + 0xA9C50; vars.AddrSaveSlots = baseAddr + 0x20B2B0; vars.AddrUsedBossWarps = baseAddr + 0x229F40; vars.DFCrystal5FinalIP = 0x174210; break; default: return; } vars.ScriptCodeStart = new DeepPointer(addrScriptMgr, 0x1C); // Initialize variable for speed display vars.curMS = 0; // Set flag for initialization of custom current. variables (see comments there for info) vars.customCurrentVarsAreInitialized = false; } update { // ==== Race condition detection + correction ==== // // This is done first thing in the cycle, to prevent corrupt map IDs from leaking in // // description of the issue we are solving: // https://discord.com/channels/313375426112389123/408694062862958592/880900162250211338 // == Manually read the map ID again (currentPlusAlpha) == // We will use it to verify LiveSplit's read (current) int currentPlusAlphaCurTribe = memory.ReadValue<int>((IntPtr)vars.AddrCurTribe); int currentPlusAlphaCurLevel = memory.ReadValue<int>((IntPtr)vars.AddrCurLevel); int currentPlusAlphaCurMap = memory.ReadValue<int>((IntPtr)vars.AddrCurMap); int currentPlusAlphaCurType = memory.ReadValue<int>((IntPtr)vars.AddrCurType); // == Race condition detection == // current is a race condition read iff old != current != currentPlusAlpha, assuming: // 1. the game is able progress between current and currentPlusAlpha // 2. LiveSplit is polling fast enough that you can't legitimately traverse through 3 different maps lol // // Here is a diagram of all possible scenarios: // // o = old, c = current, a = currentPlusAlpha // ------------ time -----------> // // And for example let's say: // old map ID = 0, 0, 5, 0 (4 ints) // new map ID = 4, 1, 1, 0 // // Haven't hit the new map yet. o == c == a == 0, 0, 5, 0 (the old map ID) // o c a // old map | new map // // currentPlusAlpha got a race condition read, so it is a mixture of the old and new ID, // for example like 0, 0, 1, 0. But current is still valid (0, 0, 5, 0), and that's all that matters // o c a // old map | new map // // currentPlusAlpha read the next map (4, 1, 1, 0). who cares. // o c a // old map | new map // // !!! current got a race condition read !!! // this is the only situation where o != c != a, so that's how we can tell // o c a // old map | new map // // This is a standard map change! o and c are squarely in the old and new maps // o c a // old map | new map // // This is not possible, because o is just c from last cycle, // and we will clean any race conditions out of c // o c a // old map | new map if (vars.HasMapIDChanged(old, current) && !vars.IsThisMap(current, currentPlusAlphaCurTribe, currentPlusAlphaCurLevel, currentPlusAlphaCurMap, currentPlusAlphaCurType)) { print("****************** RACE CONDITION DETECTED **********************\n" + old.CurTribe + ", " + old.CurLevel + ", " + old.CurMap + ", " + old.CurType + " old.CurWad\n" + current.CurTribe + ", " + current.CurLevel + ", " + current.CurMap + ", " + current.CurType + " current.CurWad\n" + currentPlusAlphaCurTribe + ", " + currentPlusAlphaCurLevel + ", " + currentPlusAlphaCurMap + ", " + currentPlusAlphaCurType + " currentPlusAlphaCurWad\n" + "******* OVERWRITING current.CurWad WITH currentPlusAlphaCurWad *******"); // == Race condition correction == // // Historically, up until now when the race condition occurred: // we split on the first map change (old map -> race condition), // then blocked the double split from the immediately following map change (race condition -> new map) // // So instead of overwriting current with old (thus delaying the map change / split until next cycle), // let's overwrite it with currentPlusAlpha (so we split right away like we historically have, // while still correcting the corrupt data!) current.CurTribe = currentPlusAlphaCurTribe; current.CurLevel = currentPlusAlphaCurLevel; current.CurMap = currentPlusAlphaCurMap; current.CurType = currentPlusAlphaCurType; } // == misc notes == // // I've assumed that the game slowly writes the 4 map ID ints one by one, // then LiveSplit suddenly comes in and reads memory while the game has its pants down. // BUT, it's possible it could be the other way around? as in: // LiveSplit slowly reads the 4 addresses one by one, then the game suddenly writes memory in the middle of that. // If that's the case, the race condition could potentially be solved simply by // reading the map ID in LiveSplit as a single 4 int block... // // Also, it's possible that "race condition" isn't the right terminology for what's happening here, // but at this point we've been calling it that for years *shrug* // // An easy place to test is at the entrance of Masher; // Just put a rubber band on your controller to hold analog stick right + pause, // and you will go hub -> cutscene -> masher -> hub in a loop. // But still it will probably take ~5 minutes to get it even once... // // ==== (end race condition detection + correction) ==== // Initialization of custom current. variables // // Unfortunately this can't simply be initialized in `startup` or `init`, // because when livesplit detects the EU version, the old/current variables get wiped. if (!vars.customCurrentVarsAreInitialized) { // Initialize PrevTribeSSX // Currently it's just used for detecting wrong warps old.PrevTribeSSX = -1; current.PrevTribeSSX = -1; // Initialize WadB4GH (Wad Before GOA or Hub) // WadB4GH is used for detecting re-entry for wrong warp, or if you GOA'd in the hub. old.TribeB4GH = -1; old.LevelB4GH = -1; old.MapB4GH = -1; old.TypeB4GH = -1; current.TribeB4GH = -1; current.LevelB4GH = -1; current.MapB4GH = -1; current.TypeB4GH = -1; vars.customCurrentVarsAreInitialized = true; } // Update addrSaveSlot const int SaveSlotSize = 0x2000; vars.addrSaveSlot = vars.AddrSaveSlots + (current.CurSaveSlotIdx * SaveSlotSize); // Update PrevTribeSSX const int offsetPrevTribeSSX = 0x2B4; var addrPrevTribeSSX = vars.addrSaveSlot + offsetPrevTribeSSX; current.PrevTribeSSX = memory.ReadValue<int>((IntPtr)addrPrevTribeSSX); // Update WadB4GH if (vars.HasMapIDChanged(old, current) && ( // now on GOA screen vars.IsThisMap(current, 0, 0, 2, 3) || // or hub (but not if we were already on GOA screen) (vars.IsGobboHub(current) && !vars.IsThisMap(old, 0, 0, 2, 3))) ) { current.TribeB4GH = old.CurTribe; current.LevelB4GH = old.CurLevel; current.MapB4GH = old.CurMap; current.TypeB4GH = old.CurType; } // Speed display if (settings["SpeedDisplay"]) { // Calculate number of in game frames since the last livesplit cycle // (there is probably some variable built into in livesplit we can replace oldMS/curMS with...) vars.oldMS = vars.curMS; vars.curMS = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; vars.ingameframes = (vars.curMS - vars.oldMS)/1000.0 / (1.0/30.0); // (units per frame) // Only looks at Croc's distance on the XZ plane (ignores vertical movement) vars.upf2D = Math.Sqrt( Math.Pow(current.CrocX-old.CrocX, 2.0) + Math.Pow(current.CrocZ-old.CrocZ, 2.0)); // Includes vertical movement (full 3D movement) vars.upf3D = Math.Sqrt( Math.Pow(current.CrocX-old.CrocX, 2.0) + Math.Pow(current.CrocZ-old.CrocZ, 2.0) + Math.Pow(current.CrocY-old.CrocY, 2.0)); // We are oversampling (livesplit is ~60fps while the game is 30fps). // If we sample the game again before it is on the next frame, then croc's position will not have changed, // so we can toss out any results where upf3D == 0 (no movement). // // (an unfortunate side effect of this is that even if croc is legitimately not moving, the display will hold the last non-zero value...) // // Conversely, if it has been any more than the length of 1 game frame since the last livesplit cycle, // then we *could* have 2 or more frames of Croc movement in our result, // so we require it has been less than 1 in game frame since the last cycle if (vars.upf3D != 0 && vars.ingameframes < 1.0) { // print("units per frame 2D: " + vars.upf2D.ToString() + // "\nunits per frame 3D: " + vars.upf3D.ToString()); vars.SetTextComponent("UPF (2D)", vars.upf2D.ToString("0.0000"), true); vars.SetTextComponent("UPF (3D)", vars.upf3D.ToString("0.0000"), true); } } // Debug output if (settings["DebugOutput"]) { string debugText = ""; // Map changes if (settings["DO_MapChanges"] && vars.HasMapIDChanged(old, current)) { debugText += "\n┃Tribe: " + old.CurTribe.ToString() + " -> " + current.CurTribe.ToString() + " (" + currentPlusAlphaCurTribe.ToString() + ")\n┃Level: " + old.CurLevel.ToString() + " -> " + current.CurLevel.ToString() + " (" + currentPlusAlphaCurLevel.ToString() + ")\n┃Map: " + old.CurMap.ToString() + " -> " + current.CurMap.ToString() + " (" + currentPlusAlphaCurMap.ToString() + ")\n┃Type: " + old.CurType.ToString() + " -> " + current.CurType.ToString() + " (" + currentPlusAlphaCurType.ToString() + ")"; } // WadB4GH changes if (settings["DO_WadB4GH"] && (old.TribeB4GH != current.TribeB4GH || old.LevelB4GH != current.LevelB4GH || old.MapB4GH != current.MapB4GH || old.TypeB4GH != current.TypeB4GH)) { debugText += "\n┃TribeB4GH: " + old.TribeB4GH.ToString() + " -> " + current.TribeB4GH.ToString() + "\n┃LevelB4GH: " + old.LevelB4GH.ToString() + " -> " + current.LevelB4GH.ToString() + "\n┃MapB4GH: " + old.MapB4GH.ToString() + " -> " + current.MapB4GH.ToString() + "\n┃TypeB4GH: " + old.TypeB4GH.ToString() + " -> " + current.TypeB4GH.ToString(); } // PrevTribeSSX changes if (settings["DO_PrevTribeSSX"] && old.PrevTribeSSX != current.PrevTribeSSX) { debugText += "\n┃PrevTribeSSX: " + old.PrevTribeSSX.ToString() + " -> " + current.PrevTribeSSX.ToString(); } // MainState changes if (settings["DO_MainState"] && old.MainState != current.MainState) { debugText += "\n┃MainState: " + old.MainState.ToString() + " -> " + current.MainState.ToString(); } // InGameState changes if (settings["DO_InGameState"] && old.InGameState != current.InGameState) { debugText += "\n┃InGameState: " + old.InGameState.ToString() + " -> " + current.InGameState.ToString(); } // IsCheatMenuOpen changes if (settings["DO_IsCheatMenuOpen"] && old.IsCheatMenuOpen != current.IsCheatMenuOpen) { debugText += "\n┃IsCheatMenuOpen: " + old.IsCheatMenuOpen.ToString() + " -> " + current.IsCheatMenuOpen.ToString(); } // AllowReturnToHub changes if (settings["DO_AllowReturnToHub"] && old.AllowReturnToHub != current.AllowReturnToHub) { debugText += "\n┃AllowReturnToHub: " + old.AllowReturnToHub.ToString() + " -> " + current.AllowReturnToHub.ToString(); } // IsMapLoaded changes if (settings["DO_IsMapLoaded"] && old.IsMapLoaded != current.IsMapLoaded) { debugText += "\n┃IsMapLoaded: " + old.IsMapLoaded.ToString() + " -> " + current.IsMapLoaded.ToString(); } // Print output if (debugText != "") { print("┏━━━━━━━━━━━━━┓" + debugText + "\n┗━━━━━━━━━━━━━┛"); } } return version != ""; } start { const int MainState_ChooseSaveSlot = 2; const int MainState_Running = 11; const int MainState_LevelSelect = 18; // Reset progress list ((IDictionary<string, object>)current).Remove("ProgressList"); // Do not start timer if any boss warp has already been used if (settings["RequireUnusedBossWarps"] && memory.ReadValue<int>((IntPtr)vars.AddrUsedBossWarps) != 0) { return false; } // Start when main state is in transition from // "level select" or "save slot selection" to "running" if (settings["SaveSlotStart"] && ( current.MainState == MainState_ChooseSaveSlot || current.MainState == MainState_LevelSelect) && current.IsNewMainStateValid != 0 && current.NewMainState == MainState_Running) { return true; } // The following start condition checks assume the game is running // and the current map is an ingame tribe and not a cutscene if (current.MainState != MainState_Running || current.CurTribe < 1 || current.CurTribe > 5 || current.CurType == 3) { return false; } // Start on any input if (settings["InputStart"] && current.Inputs != old.Inputs) { return true; } // IL start if (settings["ILstart"] && vars.HasMapIDChanged(old, current) && // Current map is a non-village map of Dante's World // or a non-village level of the Gobbo tribes (current.CurTribe == 5 ? current.CurMap > 1 : (current.CurType != 0 || current.CurLevel > 1))) { return true; } // OTS start (on hub entry) if (settings["OTSstart"] && // entering hub // (no secret village for now; its OTS isn't set up yet) !vars.IsGobboHub(old) && vars.IsGobboHub(current) && // disallow starting on cheat menu warp // (technically a valid SMP->__ segment start, but more intrusive than useful) current.IsCheatMenuOpen == 0 && // disallow starting after the opening cutscene when you start a new game !vars.IsThisMap(old, 1, 1, 2, 3) && // disallow starting after loading a save !vars.IsThisMap(old, 0, 0, 4, 3) && // disallow starting after GOA in gobbo hub or secret hub (invalid spawn) !( // was on GOA screen vars.IsThisMap(old, 0, 0, 2, 3) && // map before GOA is equal to current map (which is already constrained to hub) current.TribeB4GH == current.CurTribe && current.LevelB4GH == current.CurLevel && current.MapB4GH == current.CurMap && current.TypeB4GH == current.CurType ) && // disallow starting on doing a wrong warp (invalid spawn) !vars.IsWrongWarp(old, current)) { return true; } // OTS start (on SMP entry) if (settings["OTSstart_SMP"] && vars.HasMapIDChanged(old, current) && vars.IsShopMap(current)) { return true; } // IW start if (settings["IWstart"] && // advancing to a later village old.CurTribe < current.CurTribe && // was in one of these places ( // SMP vars.IsShopMap(old) || // anywhere, as long as the cheat menu was used old.IsCheatMenuOpen == 1 ) && // now in one of these places ( // hub of cossack, caveman, or inca (vars.IsGobboHub(current) && current.CurTribe != 1) || // hub of secret sailor vars.IsThisMap(current, 5, 2, 1, 0) )) { return true; } } split { // Split on literally any map change if (settings["SplitOnMapChange_literal"] && vars.HasMapIDChanged(old, current)) { return true; } // Cancel if main state is not "running" or // current tribe is not an ingame tribe const int MainState_Running = 11; if (current.MainState != MainState_Running || current.CurTribe < 1 || current.CurTribe > 5) { ((IDictionary<string, object>)current).Remove("ProgressList"); return false; } // Prevent IL/OTS end from being skipped when exiting via GOA // (is "old progress list" not available during this map change? which is why this must go up here?) if (settings["SplitOnMapChange"] && vars.HasMapIDChanged(old, current) && // was on GOA screen vars.IsThisMap(old, 0, 0, 2, 3) && // disallow split after GOAing in gobbo hub or secret hub (invalid spawn) !( ( vars.IsGobboHub(current) || (current.CurTribe == 5 && current.CurLevel >= 2 && current.CurLevel <= 4 && current.CurMap == 1 && current.CurType == 0) ) && // map before GOA is equal to current map (hub) current.TribeB4GH == current.CurTribe && current.LevelB4GH == current.CurLevel && current.MapB4GH == current.CurMap && current.TypeB4GH == current.CurType )) { return true; } // Prevent IL/OTS end from being skipped when exiting credits screen (ex: into Inca hub after beating Dante) // (the "old progress list" is not available when this map change happens) if (settings["SplitOnMapChange"] && vars.HasMapIDChanged(old, current) && // we came from the credits screen vars.IsThisMap(old, 0, 0, 5, 0)) { return true; } // Read progress list const int ProgressListSize = 0xf0; const int ProgressListOffset = 0x2d0; var addrProgressList = vars.addrSaveSlot + ProgressListOffset; current.ProgressList = memory.ReadBytes((IntPtr)addrProgressList, ProgressListSize); // Cancel if old progress list is not available if (!((IDictionary<string, object>)old).ContainsKey("ProgressList")) return false; // IL/OTS end if (settings["SplitOnMapChange"] && vars.HasMapIDChanged(old, current) && // disallow the split after the opening cutscene when you start a new game !vars.IsThisMap(old, 1, 1, 2, 3) && // disallow the split between dante and ending cutscene !(vars.IsThisMap(old, 4, 2, 1, 1) && vars.IsThisMap(current, 4, 2, 1, 3)) && // disallow when changing maps within soveena, flytrap, and masher !( (current.CurTribe == 1 || current.CurTribe == 3) && (current.CurLevel == 1 || current.CurLevel == 2) && old.CurMap == 1 && current.CurMap == 2 && current.CurType == 1 ) && // disallow when re-entering for wrong warp !( // General case ( vars.IsGobboHub(old) && !vars.IsGobboHub(current) && old.TribeB4GH == current.CurTribe && old.LevelB4GH == current.CurLevel && // (allow the map to be different, so this works for levels like flytrap) old.TypeB4GH == current.CurType && current.AllowReturnToHub == 1 ) || // Masher (the only wrong warpable level with a cutscene) ( // entered caveman hub from masher current.TribeB4GH == 3 && current.LevelB4GH == 2 && // (allow the map to be different, so this works for overworld or boss room) current.TypeB4GH == 1 && // entering masher from caveman hub ( // caveman hub -> masher cutscene (vars.IsThisMap(old, 3, 1, 1, 0) && vars.IsThisMap(current, 3, 1, 1, 3)) || // (caveman hub -> ) masher cutscene -> masher overworld (vars.IsThisMap(old, 3, 1, 1, 3) && vars.IsThisMap(current, 3, 2, 1, 1)) ) && // masher objective is complete current.ProgressList[129] % 2 == 1 ) ) && // disallow when doing a wrong warp (invalid spawn) !vars.IsWrongWarp(old, current)) { return true; } // IW end // note that this is a suboption of objective style splits // (because it is redundant to use it with IL style splits) if (settings["SplitOnSMPEntry"] && // was in hub vars.IsGobboHub(old) && // now in SMP vars.IsShopMap(current) && // restrict to sailor through caveman // (inca ends on yellow dante crystal; secret ends on final egg) current.CurTribe >= 1 && current.CurTribe <= 3) { return true; } // Babies subsplits if (settings["BabiesSubsplits"] && vars.IsThisMap(current, 4, 2, 1, 0) && old.GobboCounter != current.GobboCounter && settings["BabiesSubsplits_" + current.GobboCounter.ToString()]) { return true; } // Split on collecting a gem (useful for IL runs) if (settings["SplitOnGem"] && current.GemCounter == old.GemCounter+1) { return true; } // "Dante's Final Fight": Split when last crystal is placed if (vars.IsThisMap(current, 4, 2, 1, 1)) { if (old.DFCrystal5IP != current.DFCrystal5IP && current.DFCrystal5IP == vars.ScriptCodeStart.Deref<int>(game) + vars.DFCrystal5FinalIP) { return true; } } // Split on final egg (for 100% / Max%) if (vars.IsThisMap(current, 5, 4, 1, 0)) { const int SecretCavemanIdx = (5 * 40) + (4 * 4) + 0; int oldFlags = old.ProgressList[SecretCavemanIdx]; int newFlags = current.ProgressList[SecretCavemanIdx]; const int CrystalFlags = 0x1f; if ((oldFlags & ~CrystalFlags) != (newFlags & ~CrystalFlags)) { return true; } } // Objective style splits if (settings["SplitOnObjectiveCompletion"] && // (it may or may not be necessary to disable this for "Dante's Final Fight"?) !vars.IsThisMap(current, 4, 2, 1, 1)) { for (int tribe = 1; tribe <= 5; ++tribe) for (int level = 1; level <= 7; ++level) for (int type = 0; type <= 3; ++type) { // Index into progress list int i = tribe * 40 + level * 4 + type; // Skip unchanged entries int oldFlags = old.ProgressList[i], newFlags = current.ProgressList[i]; if (oldFlags == newFlags) continue; // Dante's World (Secret Village) if (tribe == 5) { const int CrystalFlags = 0x1f; // Split on both gems and eggs if (settings["SplitOnDanteCrystals"]) { return true; } // Or only split on eggs else if ((oldFlags & ~CrystalFlags) != (newFlags & ~CrystalFlags)) { return true; } } // Split on any progress change for certain levels else if ( // Boss level or secret level (as in Jigsaw) type != 0 || // "Bride of the Dungeon of Defright" or "Goo Man Chu's Tower" (tribe == 4 && (level == 5 || level == 6))) { return true; } // Other levels else { // Check for main objective and possibly Golden Gobbo int checkFlags = settings["SplitOnGoldenGobbo"] ? 5 : 1; int currentFlags = newFlags & checkFlags; // Split if all flags are set now and were not set previously if (currentFlags == checkFlags && currentFlags != (oldFlags & checkFlags)) { return true; } } } } } isLoading { const int MainState_Running = 11; return current.MainState == MainState_Running && ( current.InGameState == 6 || current.InGameState == 7 || !current.IsMapLoaded); }