// Autosplitter script for Lingo 2, by hatkirby. // // Massive thanks to the game developer, Brenton, for working with me to // make this possible. // // Example config file: // https://code.fourisland.com/autosplitters/plain/example.lingo2_config // // You must restart either the splitter or the game after choosing a config // file in the autosplitter settings, and after modifying the config file. 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.importantPanels = new HashSet(); vars.importantMultiPanels = new HashSet(); vars.importantMaps = new HashSet(); vars.importantMultiMaps = new HashSet(); vars.importantKeys = new HashSet(); vars.importantCollectibles = new HashSet(); vars.configFiles = null; vars.settings = settings; var findConfigFiles = (Action)((string folder) => { var files = new List(); if (folder != null) { vars.log("Searching for config files in '" + folder + "'"); files.AddRange(System.IO.Directory.GetFiles(folder, "*.lingo2_config")); files.AddRange(System.IO.Directory.GetFiles(folder, "*.lingo2_config.txt")); files.AddRange(System.IO.Directory.GetFiles(folder, "*.lingo2_conf")); files.AddRange(System.IO.Directory.GetFiles(folder, "*.lingo2_confi")); vars.log("Found " + files.Count + " config files"); } // Only add the parent setting the first time we call this function if (vars.configFiles == null) { vars.configFiles = new Dictionary(); vars.settings.Add("configs", (files.Count > 0), "Split based on configuration file:"); } foreach (var file in files) { string fileName = file.Split('\\').Last(); if (vars.configFiles.ContainsKey(fileName)) continue; vars.configFiles[fileName] = file; vars.settings.Add(fileName, false, null, "configs"); } }); // Search for config files relative to LiveSplit.exe findConfigFiles(Directory.GetCurrentDirectory()); // Search for config files relative to the current layout findConfigFiles(System.IO.Path.GetDirectoryName(timer.Layout.FilePath)); // Search for config files relative to the current splits findConfigFiles(System.IO.Path.GetDirectoryName(timer.Run.FilePath)); 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"))); if (settings["configs"]) { string[] lines = {""}; foreach (var configFile in vars.configFiles.Keys) { if (settings[configFile]) { // Full path is saved in the dictionary. lines = System.IO.File.ReadAllLines(vars.configFiles[configFile]); vars.log("Selected config file: " + configFile); break; } } if (lines.Length == 0) { vars.log("Config file empty or no config file selected!"); } else { string mode = ""; for (int i=0; i 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"] || vars.importantPanels.Contains(vars.lastPanel) || vars.importantMultiPanels.Contains(vars.lastPanel)) { should_split = true; } if (settings["panels_first"] || vars.importantPanels.Contains(vars.lastPanel)) { 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; } if (!should_split && vars.currentMap.Current != vars.prevMap) { if (settings["maps"] || vars.importantMaps.Contains(vars.currentMap.Current) || vars.importantMultiMaps.Contains(vars.currentMap.Current)) { should_split = true; } if (settings["maps_first"] || vars.importantMaps.Contains(vars.currentMap.Current)) { 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 (vars.currentMap.Current == "credits") { should_split = false; } if (should_split) { vars.log("Split on map change: " + vars.currentMap.Current); } vars.prevMap = vars.currentMap.Current; } if (!should_split && vars.latestKeyKey != null && !vars.collectedKeys.Contains(vars.latestKeyKey)) { if (settings["keys"] || vars.importantKeys.Contains(vars.latestKeyKey)) { should_split = true; vars.log("Split on collected key: " + vars.latestKeyKey); } vars.collectedKeys.Add(vars.latestKeyKey); } if (!should_split && vars.latestCollectible.Current != vars.latestCollectible.Old) { if (vars.importantCollectibles.Contains(vars.latestCollectible.Current)) { should_split = true; vars.log("Split on configured collectible: " + vars.latestCollectible.Current); } else if (vars.latestCollectible.Current.EndsWith("Painting")) { if (settings["paintings"]) { should_split = true; vars.log("Split on unlocked painting: " + vars.latestCollectible.Current); } } else if (vars.latestCollectible.Current.StartsWith("grave_")) { if (settings["graves"]) { should_split = true; vars.log("Split on completed gravestone: " + vars.latestCollectible.Current); } } else if (vars.latestCollectible.Current.EndsWith("_mastery")) { if (settings["masteries"]) { should_split = true; vars.log("Split on collected mastery: " + vars.latestCollectible.Current); } } else if (vars.latestCollectible.Current.EndsWith("_ending") && vars.latestCollectible.Current != "free_ending") { if (settings["ends"]) { should_split = true; vars.log("Split on ending: " + vars.latestCollectible.Current); } } } return should_split; }