state("junkTest-Win64-Shipping") { } startup { Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Basic"); vars.Helper.GameName = "Junkster"; } init { vars.GWorld = vars.Helper.ScanRel(8, "0F 2E ?? 74 ?? 48 8B 1D ?? ?? ?? ?? 48 85 DB 74"); vars.Log("GWorld: 0x" + vars.GWorld.ToString("X")); vars.FNamePool = vars.Helper.ScanRel(13, "89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15"); vars.Log("FNamePool: 0x" + vars.FNamePool.ToString("X")); // cached once we've discovered ComicFrontend's raw FName value - avoids repeated string decoding vars.ComicFrontendFName = null; // classification caches for detecting "actually in a level" vs hub/menu/cutscene vars.NonPuzzleFNames = new HashSet(); // hub + cutscene sublevel names, once identified vars.PuzzleFNames = new HashSet(); // any other sublevel name, once identified as real puzzle content var cachedFNames = new Dictionary(); vars.ReadFName = (Func)(fname => { string name; if (cachedFNames.TryGetValue(fname, out name)) return name; int name_offset = (int) fname & 0xFFFF; int chunk_offset = (int) (fname >> 0x10) & 0xFFFF; var base_ptr = new DeepPointer((IntPtr) vars.FNamePool + chunk_offset * 0x8 + 0x10, name_offset * 0x2); byte[] name_metadata = base_ptr.DerefBytes(game, 2); if (name_metadata == null) return null; int size = name_metadata[1] << 2 | (name_metadata[0] & 0xC0) >> 6; IntPtr name_addr; base_ptr.DerefOffsets(game, out name_addr); name = game.ReadString(name_addr + 0x2, size); cachedFNames[fname] = name; return name; }); } update { if (vars.GWorld == IntPtr.Zero || vars.FNamePool == IntPtr.Zero) return; try { IntPtr world = vars.Helper.Read((IntPtr) vars.GWorld); if (world == IntPtr.Zero) return; // Levels TArray: data ptr at +0x138, count at +0x140 IntPtr levelsData = vars.Helper.Read(world + 0x138); int levelsCount = vars.Helper.Read(world + 0x140); bool foundComicFrontend = false; bool foundPuzzleContent = false; if (levelsData != IntPtr.Zero && levelsCount > 0 && levelsCount < 64) // sanity clamp against a bad read { for (int i = 0; i < levelsCount; i++) { IntPtr level = vars.Helper.Read(levelsData + i * 0x8); if (level == IntPtr.Zero) continue; IntPtr outer = vars.Helper.Read(level + 0x20); // UOBJECT_OUTER if (outer == IntPtr.Zero) continue; long outerFName = vars.Helper.Read(outer + 0x18); if (vars.ComicFrontendFName != null) { // fast path: cheap integer compare, no string decode if (outerFName == (long) vars.ComicFrontendFName) { foundComicFrontend = true; } } else { // slow path: only runs until we've discovered and cached the target FName once string name = vars.ReadFName(outerFName); if (name == "ComicFrontend") { vars.ComicFrontendFName = outerFName; foundComicFrontend = true; } } // classify this sublevel: known hub/cutscene name, or real puzzle content? if (!vars.NonPuzzleFNames.Contains(outerFName) && !vars.PuzzleFNames.Contains(outerFName)) { string classifyName = vars.ReadFName(outerFName); bool isKnownNonPuzzle = classifyName == "StrmMaster_Adventure" || classifyName == "AlienOverworld_LightingScenario" || classifyName == "AlienOverworld_DioramaArt" || classifyName == "AlienOverworld_Main" || classifyName == "ComicFrontend" || classifyName == "AlienOverworld_ending_cutscene" || classifyName == "AlienOverworld_FinalCutsceneEnvironment"; if (isKnownNonPuzzle) vars.NonPuzzleFNames.Add(outerFName); else vars.PuzzleFNames.Add(outerFName); } if (vars.PuzzleFNames.Contains(outerFName)) foundPuzzleContent = true; } } current.inLevel = foundPuzzleContent; current.puzzleEnding = foundComicFrontend; if (!((IDictionary) old).ContainsKey("puzzleEnding")) { vars.Log("puzzleEnding (initial): " + current.puzzleEnding); return; } if (old.puzzleEnding != current.puzzleEnding) { vars.Log("puzzleEnding: " + old.puzzleEnding + " -> " + current.puzzleEnding); } // --- Save-file total playtime: GWorld -> GameState -> SaveManager -> CurrentGlobalSaveData --- IntPtr gameState = vars.Helper.Read(world + 0x120); // UWorld::GameState if (gameState != IntPtr.Zero) { IntPtr saveManager = vars.Helper.Read(gameState + 0x4B0); if (saveManager != IntPtr.Zero) { // Current level's live timer (SaveManager -> CurrentLevelSaveData -> TimeSave.LevelTimeSecs). // Exposed as an ASL var (current.levelTimeSecs) for binding to a LiveSplit Text component - // not used internally, gameTime runs off totalGamePlayTime instead. Resets to 0 the instant // you enter a level, counts up live while playing, holds its final value through the recap. IntPtr levelSaveData = vars.Helper.Read(saveManager + 0x160); if (levelSaveData != IntPtr.Zero) { current.levelTimeSecs = vars.Helper.Read(levelSaveData + 0xD0); // Uncomment for debugging: // if (!((IDictionary) old).ContainsKey("levelTimeSecs")) // vars.Log("levelTimeSecs (initial): " + current.levelTimeSecs); // else if (old.levelTimeSecs != current.levelTimeSecs) // vars.Log("levelTimeSecs: " + old.levelTimeSecs + " -> " + current.levelTimeSecs); } IntPtr globalSave = vars.Helper.Read(saveManager + 0x100); if (globalSave != IntPtr.Zero) { current.totalGamePlayTime = vars.Helper.Read(globalSave + 0x1C8); if (!((IDictionary) old).ContainsKey("totalGamePlayTime")) { vars.Log("totalGamePlayTime (initial): " + current.totalGamePlayTime); } else if (old.totalGamePlayTime != current.totalGamePlayTime) { vars.Log("totalGamePlayTime: " + old.totalGamePlayTime + " -> " + current.totalGamePlayTime); } } } } } catch (Exception e) { vars.Log("update() exception: " + e.Message); return; } } start { var currentDict = (IDictionary) current; var oldDict = (IDictionary) old; if (!oldDict.ContainsKey("inLevel") || !currentDict.ContainsKey("totalGamePlayTime")) return false; // just walked into a level, on a save that hasn't finished one yet - this can only be a fresh run bool justEnteredLevel = !old.inLevel && current.inLevel; bool looksFresh = (double) current.totalGamePlayTime < 2.0; return justEnteredLevel && looksFresh; } split { var oldDict = (IDictionary) old; if (!oldDict.ContainsKey("puzzleEnding")) return false; return !old.puzzleEnding && current.puzzleEnding; } gameTime { var currentDict = (IDictionary) current; if (!currentDict.ContainsKey("totalGamePlayTime")) return null; return TimeSpan.FromSeconds((double) current.totalGamePlayTime); }