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/) |