// Autosplitter script for Lingo 2, by hatkirby. // // Massive thanks to the game developer, Brenton, for working with me to // make this possible. state("Lingo2") {} startup { // Relative to Livesplit.exe vars.logFilePath = Directory.GetCurrentDirectory() + "\\autosplitter_lingo2.log"; vars.log = (Action)((string logLine) => { print("[Lingo 2 ASL] " + logLine); string time = System.DateTime.Now.ToString("dd/MM/yy hh:mm:ss.fff"); // AppendAllText will create the file if it doesn't exist. System.IO.File.AppendAllText(vars.logFilePath, time + ": " + logLine + "\r\n"); }); settings.Add("panels", false, "Split on solving a panel"); settings.Add("panels_first", false, "Only split the first time each panel is solved", "panels"); settings.Add("maps", false, "Split on changing maps"); settings.Add("maps_first", false, "Only split the first time a map is entered", "maps"); settings.Add("ends", false, "Split on any ending"); settings.Add("keys", false, "Split on unlocking a key"); settings.Add("paintings", false, "Split on unlocking a gallery painting"); settings.Add("graves", false, "Split on completing a gravestone"); settings.Add("masteries", false, "Split on collecting a mastery"); vars.prevPanel = null; vars.lastPanel = null; vars.solvedPanels = new HashSet(); vars.prevMap = ""; vars.visitedMaps = new HashSet(); vars.collectedKeys = new HashSet(); vars.latestKeyKey = null; vars.log("Autosplitter loaded"); } init { // magic byte array format: // [0-7]: 9c 46 9f b0 4b 6a e0 8d (random bytes used for sigscanning) // [8]: Loading flag // [9]: First movement flag // [10]: Latest unlocked key (0 if run just started) // [11]: 1 or 2, indicating the level of the latest unlocked key // [12-52]: Latest collectable unlocked (null terminated, truncated at 40 chars excluding null) // [53-93]: Current map name (null terminated, truncated at 40 chars excluding null) // [94-134]: Map name of latest solved panel (null terminated, truncated at 40 chars excluding null) // [135-235]: Full nodepath of latest solved panel (null terminated, truncated at 100 chars excluding null) IntPtr ptr = IntPtr.Zero; foreach (var page in game.MemoryPages(true).Reverse()) { var scanner = new SignatureScanner(game, page.BaseAddress, (int)page.RegionSize); ptr = scanner.Scan(new SigScanTarget(0, "9c 46 9f b0 4b 6a e0 8d")); if (ptr != IntPtr.Zero) { break; } } if (ptr == IntPtr.Zero) { throw new Exception("Could not find magic autosplitter array!"); } vars.loading = new MemoryWatcher(ptr + 8); vars.firstInput = new MemoryWatcher(ptr + 9); vars.latestKey = new MemoryWatcher(ptr + 10); vars.latestKeyLevel = new MemoryWatcher(ptr + 11); vars.latestCollectible = new StringWatcher(ptr + 12, 41); vars.currentMap = new StringWatcher(ptr + 53, 41); vars.lastPanelMap = new StringWatcher(ptr + 94, 41); vars.lastPanelPath = new StringWatcher(ptr + 135, 101); vars.log(String.Format("Magic autosplitter array: {0}", ptr.ToString("X"))); } update { vars.loading.Update(game); vars.firstInput.Update(game); vars.latestKey.Update(game); vars.latestKeyLevel.Update(game); vars.latestCollectible.Update(game); vars.currentMap.Update(game); vars.lastPanelMap.Update(game); vars.lastPanelPath.Update(game); if (vars.lastPanelMap.Current != "") { vars.lastPanel = string.Format("{0}/{1}", vars.lastPanelMap.Current, vars.lastPanelPath.Current); } if (vars.latestKeyLevel.Current > 0) { vars.latestKeyKey = string.Format("{0}{1}", (char)vars.latestKey.Current, vars.latestKeyLevel.Current); } } start { return vars.firstInput.Old == 0 && vars.firstInput.Current == 1; } onStart { vars.prevPanel = vars.lastPanel; vars.prevMap = vars.currentMap.Current; vars.lastPanel = null; vars.solvedPanels.Clear(); vars.visitedMaps.Clear(); vars.visitedMaps.Add(vars.currentMap.Current); vars.collectedKeys.Clear(); vars.latestKeyKey = null; } reset { return vars.firstInput.Old == 1 && vars.firstInput.Current == 0; } isLoading { return vars.loading.Current == 1; } split { bool should_split = false; if (vars.lastPanel != vars.prevPanel) { if (settings["panels"]) { should_split = true; } if (settings["panels_first"]) { if (vars.solvedPanels.Contains(vars.lastPanel)) { should_split = false; vars.log("Already solved panel: " + vars.lastPanel); } else { vars.solvedPanels.Add(vars.lastPanel); } } if (should_split) { vars.log("Split on panel: " + vars.lastPanel); } vars.prevPanel = vars.lastPanel; } else if (vars.currentMap.Current != vars.prevMap) { if (settings["maps"]) { should_split = true; } if (settings["maps_first"]) { if (vars.visitedMaps.Contains(vars.currentMap.Current)) { should_split = false; vars.log("Already visited map: " + vars.currentMap.Current); } else { vars.visitedMaps.Add(vars.currentMap.Current); } } if (should_split) { vars.log("Split on map change: " + vars.currentMap.Current); } vars.prevMap = vars.currentMap.Current; } else if (vars.latestKeyKey != null && !vars.collectedKeys.Contains(vars.latestKeyKey)) { if (settings["keys"]) { should_split = true; vars.log("Split on collected key: " + vars.latestKeyKey); } vars.collectedKeys.Add(vars.latestKeyKey); } return should_split; }