diff options
| -rw-r--r-- | Lingo 2.asl | 145 | ||||
| -rw-r--r-- | README.md | 1 |
2 files changed, 146 insertions, 0 deletions
| diff --git a/Lingo 2.asl b/Lingo 2.asl new file mode 100644 index 0000000..9c59387 --- /dev/null +++ b/Lingo 2.asl | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | // Autosplitter script for Lingo 2, by hatkirby. | ||
| 2 | // | ||
| 3 | // Massive thanks to the game developer, Brenton, for working with me to | ||
| 4 | // make this possible. | ||
| 5 | |||
| 6 | state("Lingo2") {} | ||
| 7 | |||
| 8 | startup | ||
| 9 | { | ||
| 10 | // Relative to Livesplit.exe | ||
| 11 | vars.logFilePath = Directory.GetCurrentDirectory() + "\\autosplitter_lingo2.log"; | ||
| 12 | vars.log = (Action<string>)((string logLine) => { | ||
| 13 | print("[Lingo 2 ASL] " + logLine); | ||
| 14 | string time = System.DateTime.Now.ToString("dd/MM/yy hh:mm:ss.fff"); | ||
| 15 | // AppendAllText will create the file if it doesn't exist. | ||
| 16 | System.IO.File.AppendAllText(vars.logFilePath, time + ": " + logLine + "\r\n"); | ||
| 17 | }); | ||
| 18 | |||
| 19 | settings.Add("panels", false, "Split on solving a panel"); | ||
| 20 | settings.Add("panels_first", false, "Only split the first time each panel is solved", "panels"); | ||
| 21 | settings.Add("maps", false, "Split on changing maps"); | ||
| 22 | settings.Add("maps_first", false, "Only split the first time a map is entered", "maps"); | ||
| 23 | settings.Add("ends", false, "Split on any ending"); | ||
| 24 | settings.Add("keys", false, "Split on unlocking a key"); | ||
| 25 | settings.Add("paintings", false, "Split on unlocking a gallery painting"); | ||
| 26 | settings.Add("graves", false, "Split on completing a gravestone"); | ||
| 27 | settings.Add("masteries", false, "Split on collecting a mastery"); | ||
| 28 | |||
| 29 | vars.prevPanelMap = ""; | ||
| 30 | vars.prevPanelPath = ""; | ||
| 31 | vars.prevMap = ""; | ||
| 32 | vars.collectedKeys = new HashSet<string>(); | ||
| 33 | vars.latestKeyKey = null; | ||
| 34 | |||
| 35 | vars.log("Autosplitter loaded"); | ||
| 36 | } | ||
| 37 | |||
| 38 | init | ||
| 39 | { | ||
| 40 | // magic byte array format: | ||
| 41 | // [0-7]: 9c 46 9f b0 4b 6a e0 8d (random bytes used for sigscanning) | ||
| 42 | // [8]: Loading flag | ||
| 43 | // [9]: First movement flag | ||
| 44 | // [10]: Latest unlocked key (0 if run just started) | ||
| 45 | // [11]: 1 or 2, indicating the level of the latest unlocked key | ||
| 46 | // [12-52]: Latest collectable unlocked (null terminated, truncated at 40 chars excluding null) | ||
| 47 | // [53-93]: Current map name (null terminated, truncated at 40 chars excluding null) | ||
| 48 | // [94-134]: Map name of latest solved panel (null terminated, truncated at 40 chars excluding null) | ||
| 49 | // [135-235]: Full nodepath of latest solved panel (null terminated, truncated at 100 chars excluding null) | ||
| 50 | IntPtr ptr = IntPtr.Zero; | ||
| 51 | foreach (var page in game.MemoryPages(true).Reverse()) { | ||
| 52 | var scanner = new SignatureScanner(game, page.BaseAddress, (int)page.RegionSize); | ||
| 53 | ptr = scanner.Scan(new SigScanTarget(0, "9c 46 9f b0 4b 6a e0 8d")); | ||
| 54 | if (ptr != IntPtr.Zero) { | ||
| 55 | break; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | if (ptr == IntPtr.Zero) { | ||
| 59 | throw new Exception("Could not find magic autosplitter array!"); | ||
| 60 | } | ||
| 61 | vars.loading = new MemoryWatcher<byte>(ptr + 8); | ||
| 62 | vars.firstInput = new MemoryWatcher<byte>(ptr + 9); | ||
| 63 | vars.latestKey = new MemoryWatcher<byte>(ptr + 10); | ||
| 64 | vars.latestKeyLevel = new MemoryWatcher<byte>(ptr + 11); | ||
| 65 | vars.latestCollectible = new StringWatcher(ptr + 12, 41); | ||
| 66 | vars.currentMap = new StringWatcher(ptr + 53, 41); | ||
| 67 | vars.lastPanelMap = new StringWatcher(ptr + 94, 41); | ||
| 68 | vars.lastPanelPath = new StringWatcher(ptr + 135, 101); | ||
| 69 | |||
| 70 | vars.log(String.Format("Magic autosplitter array: {0}", ptr.ToString("X"))); | ||
| 71 | } | ||
| 72 | |||
| 73 | update | ||
| 74 | { | ||
| 75 | vars.loading.Update(game); | ||
| 76 | vars.firstInput.Update(game); | ||
| 77 | vars.latestKey.Update(game); | ||
| 78 | vars.latestKeyLevel.Update(game); | ||
| 79 | vars.latestCollectible.Update(game); | ||
| 80 | vars.currentMap.Update(game); | ||
| 81 | vars.lastPanelMap.Update(game); | ||
| 82 | vars.lastPanelPath.Update(game); | ||
| 83 | |||
| 84 | if (vars.latestKeyLevel.Current > 0) | ||
| 85 | { | ||
| 86 | vars.latestKeyKey = string.Format("{0}{1}", (char)vars.latestKey.Current, vars.latestKeyLevel.Current); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | start | ||
| 91 | { | ||
| 92 | return vars.firstInput.Old == 0 && vars.firstInput.Current == 1; | ||
| 93 | } | ||
| 94 | |||
| 95 | onStart | ||
| 96 | { | ||
| 97 | vars.prevPanelMap = vars.lastPanelMap.Current; | ||
| 98 | vars.prevPanelPath = vars.lastPanelPath.Current; | ||
| 99 | vars.prevMap = vars.currentMap.Current; | ||
| 100 | } | ||
| 101 | |||
| 102 | reset | ||
| 103 | { | ||
| 104 | return vars.firstInput.Old == 1 && vars.firstInput.Current == 0; | ||
| 105 | } | ||
| 106 | |||
| 107 | isLoading | ||
| 108 | { | ||
| 109 | return vars.loading.Current == 1; | ||
| 110 | } | ||
| 111 | |||
| 112 | split | ||
| 113 | { | ||
| 114 | bool should_split = false; | ||
| 115 | |||
| 116 | if (vars.lastPanelMap.Current != vars.prevPanelMap || vars.lastPanelPath.Current != vars.prevPanelPath) { | ||
| 117 | if (settings["panels"]) { | ||
| 118 | should_split = true; | ||
| 119 | vars.log("Split on any panel: " + vars.lastPanelMap.Current + " / " + vars.lastPanelPath.Current); | ||
| 120 | } | ||
| 121 | |||
| 122 | vars.prevPanelMap = vars.lastPanelMap.Current; | ||
| 123 | vars.prevPanelPath = vars.lastPanelPath.Current; | ||
| 124 | } | ||
| 125 | else if (vars.currentMap.Current != vars.prevMap) | ||
| 126 | { | ||
| 127 | if (settings["maps"]) { | ||
| 128 | should_split = true; | ||
| 129 | vars.log("Split on map change: " + vars.currentMap.Current); | ||
| 130 | } | ||
| 131 | |||
| 132 | vars.prevMap = vars.currentMap.Current; | ||
| 133 | } | ||
| 134 | else if (vars.latestKeyKey != null && !vars.collectedKeys.Contains(vars.latestKeyKey)) | ||
| 135 | { | ||
| 136 | if (settings["keys"]) { | ||
| 137 | should_split = true; | ||
| 138 | vars.log("Split on collected key: " + vars.latestKeyKey); | ||
| 139 | } | ||
| 140 | |||
| 141 | vars.collectedKeys.Add(vars.latestKeyKey); | ||
| 142 | } | ||
| 143 | |||
| 144 | return should_split; | ||
| 145 | } | ||
| diff --git a/README.md b/README.md index 041270d..82c67d5 100644 --- a/README.md +++ b/README.md | |||
| @@ -6,6 +6,7 @@ These are automatic splitter scripts for the speedrunning timer application, | |||
| 6 | I have written autosplitters for the following games: | 6 | I have written autosplitters for the following games: |
| 7 | 7 | ||
| 8 | - [Lingo](https://lingothegame.com/) | 8 | - [Lingo](https://lingothegame.com/) |
| 9 | - [Lingo 2](https://store.steampowered.com/app/2523310/Lingo_2/) | ||
| 9 | - [Manifold Garden](https://manifold.garden/) | 10 | - [Manifold Garden](https://manifold.garden/) |
| 10 | - [Taiji](https://store.steampowered.com/app/1141580/Taiji/) | 11 | - [Taiji](https://store.steampowered.com/app/1141580/Taiji/) |
| 11 | - [Temple of Starlight](https://store.steampowered.com/app/1970050/Temple_of_Starlight/) | 12 | - [Temple of Starlight](https://store.steampowered.com/app/1970050/Temple_of_Starlight/) |
