diff options
-rw-r--r-- | Manifold Garden.asl | 206 |
1 files changed, 155 insertions, 51 deletions
diff --git a/Manifold Garden.asl b/Manifold Garden.asl index a5009ad..71cb203 100644 --- a/Manifold Garden.asl +++ b/Manifold Garden.asl | |||
@@ -1,79 +1,164 @@ | |||
1 | // AutoSplit script for Manifold Garden 1.0.30.14704 | 1 | // AutoSplit script for Manifold Garden |
2 | // | 2 | // |
3 | // Written by hatkirby, with help from preshing and Gelly. | 3 | // Written by hatkirby, with help from preshing and Gelly. |
4 | // | 4 | // |
5 | // Automatically starts the timer when a new game is started. You must still reset the timer | 5 | // Automatically starts the timer when a new game is started. You must still |
6 | // manually between runs. | 6 | // reset the timer manually between runs. |
7 | // | 7 | // |
8 | // A split is also triggered after being in one of the ending cutscenes for 1.1 seconds, | 8 | // A split is also triggered after being in one of the ending cutscenes for 1.1 |
9 | // since this is when the kaleidoscope appears. | 9 | // seconds, since this is when the kaleidoscope appears. |
10 | // | 10 | // |
11 | // If you check "All God Cubes waypoints" in the script's Advanced settings (below), the script | 11 | // If you check "All God Cubes waypoints" in the script's Advanced settings |
12 | // will only split at mandala scenes. This is useful when running "All God Cubes" categories. | 12 | // (below), the script will only split at mandala scenes. This is useful when |
13 | // running "All God Cubes" categories. | ||
13 | // | 14 | // |
14 | // The pointer path to the current level often changes when a new version of Manifold Garden is | 15 | // This should be mostly version-independent, but it works best on 1.0.0.14704 |
15 | // released. When that happens, a new pointer path must be found using CheatEngine. If the | 16 | // (Steam "Speedrunning Branch"). |
16 | // current pointer path stops working (even for a frame or two), a message is logged to the | ||
17 | // debug output. | ||
18 | // | 17 | // |
19 | // To view debug output (print statements from this script), use DebugView: | 18 | // To view debug output (print statements from this script), use DebugView: |
20 | // https://technet.microsoft.com/en-us/Library/bb896647.aspx | 19 | // https://technet.microsoft.com/en-us/Library/bb896647.aspx |
21 | 20 | ||
22 | state("ManifoldGarden") { | 21 | state("ManifoldGarden") {} |
23 | // These pointer paths seem to work with Manifold Garden 1.1.0.14704 (2020-11-09): | ||
24 | int level: "mono-2.0-bdwgc.dll", 0x00494DC8, 0x48, 0x120, 0x120, 0x120, 0x120, 0x120, 0xC60, 0x1A0; | ||
25 | bool transitionFadeHeld: "UnityPlayer.dll", 0x017945A8, 0x80, 0x10, 0x48, 0xA0, 0x10, 0xE40; | ||
26 | bool isLoadingGameFromUI: "UnityPlayer.dll", 0x017945A8, 0x90, 0x100, 0xC0, 0xC0, 0xC0, 0xC0, 0xDC1; | ||
27 | bool startScreenActive: "UnityPlayer.dll", 0x0178BBC0, 0x3B8, 0x38, 0x18, 0x8, 0x198, 0x0, 0x8ab; | ||
28 | |||
29 | // Older pointer paths: | ||
30 | //int level: "UnityPlayer.dll", 0x014BE300, 0x60, 0xA8, 0x38, 0x30, 0xB0, 0x118, 0x5C; // 1.0.30.13294 (2020-02-25) | ||
31 | //int level: "UnityPlayer.dll", 0x01552858, 0x8, 0x0, 0xB8, 0x80, 0x80, 0x28, 0x5C; // 13294 | ||
32 | //int level: "UnityPlayer.dll", 0x01552858, 0x28, 0x8, 0xB8, 0x80, 0x80, 0x28, 0x5C; // 13294 | ||
33 | //int level: "UnityPlayer.dll", 0x01507BE0, 0x0, 0x928, 0x38, 0x30, 0xB0, 0x118, 0x5C; // 1.0.29.12904 (2020-02-??), 1.0.29.12830 (2019-12-18), 1.0.29.12781 (2019-12-11) | ||
34 | //int level: "UnityPlayer.dll", 0x01507C68, 0x8, 0x38, 0xA8, 0x58, 0x118, 0x5C; | ||
35 | } | ||
36 | 22 | ||
37 | startup { | 23 | startup { |
24 | var bytes = File.ReadAllBytes(@"Components\LiveSplit.ASLHelper.bin"); | ||
25 | var type = Assembly.Load(bytes).GetType("ASLHelper.Unity"); | ||
26 | vars.Helper = Activator.CreateInstance(type, timer, this); | ||
27 | vars.Helper.LoadSceneManager = true; | ||
28 | |||
38 | settings.Add("every",true,"Split on every level change"); | 29 | settings.Add("every",true,"Split on every level change"); |
39 | settings.Add("fall",false,"Including all ending falling scenes","every"); | 30 | settings.Add("fall",false,"Including all ending falling scenes","every"); |
40 | settings.Add("allGodCubes", false, "All God Cubes waypoints"); | 31 | settings.Add("allGodCubes", false, "All God Cubes waypoints"); |
41 | settings.Add("zero", false, "Zero% waypoints"); | 32 | settings.Add("zero", false, "Zero% waypoints"); |
42 | settings.Add("raymarchitecture", true, "Split on Raymarchitecture (ending cutscene)"); | 33 | settings.Add("raymarchitecture", true, "Split on Raymarchitecture (ending cutscene)"); |
43 | settings.Add("norepeats",false,"Split only on the first encounter of each level"); | 34 | settings.Add("norepeats",false,"Split only on the first encounter of each level"); |
35 | |||
44 | vars.waypoints = null; | 36 | vars.waypoints = null; |
45 | vars.prevLevel = 0; | 37 | vars.prevLevel = 0; |
46 | vars.stopwatch = null; // Used for the final split | 38 | vars.stopwatch = null; // Used for the final split |
47 | vars.prev = new List<int>(); | 39 | vars.prev = new List<int>(); |
48 | vars.firstRoom = false; | 40 | vars.firstRoom = false; |
49 | vars.fall = new List<int>{97, 98, 99, 101, 102, 103, 104}; | 41 | vars.inEnding = false; |
42 | vars.noSplitScenes = new List<String>{ | ||
43 | "StudioLogoScreen", | ||
44 | "RequiredComponents", | ||
45 | "StartScreen_01", | ||
46 | "StartScreen_02", | ||
47 | "StartScreen_03", | ||
48 | "StartScreen_15", | ||
49 | "StartScreen_51", | ||
50 | "StartScreen_53", | ||
51 | "StartScreen_63" | ||
52 | }; | ||
53 | vars.startScreens = new List<String>{ | ||
54 | "StartScreen_01", | ||
55 | "StartScreen_02", | ||
56 | "StartScreen_03", | ||
57 | "StartScreen_15", | ||
58 | "StartScreen_51", | ||
59 | "StartScreen_53", | ||
60 | "StartScreen_63" | ||
61 | }; | ||
62 | vars.endings = new List<string>{ | ||
63 | "World_905_EndingCollapseCutscene_Optimized", | ||
64 | "World_907_EndingZeroCollapseCutscene_Optimized", | ||
65 | "World_906_EndingDarkCollapseCutscene_Optimized" | ||
66 | }; | ||
67 | vars.fall = new List<string>{ | ||
68 | "World_903_EndingFallTwo_Optimized", | ||
69 | "World_904_EndingFallThree_Optimized", | ||
70 | "World_905_EndingCollapseCutscene_Optimized", | ||
71 | "World_923_AlternateEndingFallTwo_Optimized", | ||
72 | "World_924_AlternateEndingFallThree_Optimized", | ||
73 | "World_907_EndingZeroCollapseCutscene_Optimized", | ||
74 | "World_906_EndingDarkCollapseCutscene_Optimized" | ||
75 | }; | ||
76 | vars.mandalaScenes = new List<string>{ | ||
77 | "AudioVisual_001_Optimized", | ||
78 | "AudioVisual_002_Optimized", | ||
79 | "AudioVisual_053_Optimized", | ||
80 | "AudioVisual_051_Optimized", | ||
81 | "AudioVisual_003_Optimized", | ||
82 | "AudioVisual_063_Optimized", | ||
83 | "AudioVisual_071_Optimized" | ||
84 | }; | ||
85 | vars.zeroPercentPoints = new List<string>{ | ||
86 | "Hallway_W000_W052_Optimized", | ||
87 | "World_002_Optimized", | ||
88 | "Hallway_W026_W015_Optimized", | ||
89 | "Hallway_W612_W057_Optimized", | ||
90 | "Hallway_W073_W026_Optimized", | ||
91 | "World_804_Optimized", | ||
92 | "World_071_AkshardhamTemple_Optimized" | ||
93 | }; | ||
50 | } | 94 | } |
51 | 95 | ||
52 | init { | 96 | init { |
53 | print(String.Format("**** AUTOSPLIT: Game found, pointer path {0} ****", | 97 | vars.studioScreenDone = true; |
54 | current.level == 0 ? "DOESN'T work (this is normal at startup)" : "works")); | 98 | vars.doneFirstLook = false; |
99 | vars.Helper.TryOnLoad = (Func<dynamic, bool>)(mono => | ||
100 | { | ||
101 | var gameMan = mono.GetClass("GameManager"); | ||
102 | vars.Helper["isLoadingGameFromUI"] = gameMan.Make<bool>("isLoadingGameFromUI"); | ||
103 | |||
104 | var versionNum = mono.GetClass("VersionNumber"); | ||
105 | vars.Helper["version"] = versionNum.MakeString("instance", "_text"); | ||
106 | |||
107 | current.onStartScreen = false; | ||
108 | |||
109 | return true; | ||
110 | }); | ||
111 | |||
112 | vars.Helper.Load(); | ||
55 | } | 113 | } |
56 | 114 | ||
57 | update { | 115 | update { |
58 | // Log a message when the pointer path starts/stops working: | 116 | if (!vars.Helper.Update()) |
59 | if (current.level == 0 && old.level != 0) { | 117 | return false; |
60 | print("**** AUTOSPLIT: Pointer path STOPPED working ****"); | 118 | |
61 | } else if (current.level != 0 && old.level == 0) { | 119 | current.level = vars.Helper.Scenes.Active.Index; |
62 | print("**** AUTOSPLIT: Pointer path STARTED working ****"); | 120 | current.isLoadingGameFromUI = vars.Helper["isLoadingGameFromUI"].Current; |
121 | |||
122 | if (!vars.doneFirstLook) { | ||
123 | vars.doneFirstLook = true; | ||
124 | print(String.Format("Connected to Manifold Garden version {0}", vars.Helper["version"].Current)); | ||
125 | |||
126 | current.onStartScreen = vars.startScreens.Contains(vars.Helper.Scenes.Active.Name); | ||
127 | |||
128 | // The "isLoadingGameFromUI" boolean is set while the studio screen is | ||
129 | // showing during game startup, which means that if the autosplitter is | ||
130 | // running before the game opens, it'll erroneously start a run. To | ||
131 | // avoid this, when the autosplitter initialises, we check if we are on | ||
132 | // a noSplitScene (the game usually reports itself as being on a start | ||
133 | // screen rather than the studio screen) and if the game is loading. If | ||
134 | // so, we disable starting a run until the load is complete. If the | ||
135 | // splitter is opened after the game starts up, this shouldn't activate, | ||
136 | // which means run starting should work as expected. | ||
137 | if (vars.noSplitScenes.Contains(vars.Helper.Scenes.Active.Name) | ||
138 | && current.isLoadingGameFromUI) { | ||
139 | vars.studioScreenDone = false; | ||
140 | } | ||
141 | } else { | ||
142 | if (current.level != old.level) { | ||
143 | current.onStartScreen = vars.startScreens.Contains(vars.Helper.Scenes.Active.Name); | ||
144 | } | ||
145 | if (!vars.studioScreenDone) { | ||
146 | vars.studioScreenDone = !current.isLoadingGameFromUI; | ||
147 | } | ||
63 | } | 148 | } |
64 | } | 149 | } |
65 | 150 | ||
66 | start { | 151 | start { |
67 | // Start the timer as soon as a game is being loaded (specifically the moment you click | 152 | // Start the timer as soon as a game is being loaded (specifically the |
68 | // a save slot to start a new game in, although it will also start if you just load a file). | 153 | // moment you click a save slot to start a new game in, although it will |
69 | // This boolean is set to true during the studio logo when the game starts up, so we check | 154 | // also start if you just load a file). This boolean is set to true during |
70 | // for that as well. | 155 | // the studio logo when the game starts up, so we check for that as well. |
71 | if (current.transitionFadeHeld && current.isLoadingGameFromUI) { | 156 | if (vars.studioScreenDone && current.isLoadingGameFromUI) { |
72 | print(String.Format("Level changed from {0} to {1}: START", old.level, current.level)); | 157 | print(String.Format("Level changed from {0} to {1}: START", old.level, current.level)); |
73 | if (settings["zero"]) { | 158 | if (settings["zero"]) { |
74 | vars.waypoints = new List<int>{106, 17, 110, 115, 111, 36, 44}; | 159 | vars.waypoints = vars.zeroPercentPoints; |
75 | } else if (settings["allGodCubes"]) { | 160 | } else if (settings["allGodCubes"]) { |
76 | vars.waypoints = new List<int>{82, 83, 84, 85, 86, 87, 88}; | 161 | vars.waypoints = vars.mandalaScenes; |
77 | } else { | 162 | } else { |
78 | vars.waypoints = null; | 163 | vars.waypoints = null; |
79 | } | 164 | } |
@@ -81,26 +166,31 @@ start { | |||
81 | vars.stopwatch = Stopwatch.StartNew(); | 166 | vars.stopwatch = Stopwatch.StartNew(); |
82 | vars.prev.Clear(); | 167 | vars.prev.Clear(); |
83 | vars.firstRoom = false; | 168 | vars.firstRoom = false; |
169 | vars.inEnding = false; | ||
84 | return true; | 170 | return true; |
85 | } | 171 | } |
86 | } | 172 | } |
87 | 173 | ||
88 | split { | 174 | split { |
89 | // Split when level index changes. We don't split for the first room change in a run, | 175 | // Split when level index changes. We don't split for the first room change |
90 | // because that is always going to be changing from -1 to 9, and it happens a couple of | 176 | // in a run, because that is always going to be changing from 3 to 73, and |
91 | // seconds after the timer starts. | 177 | // it happens a couple of seconds after the timer starts. |
92 | if (vars.firstRoom && current.level != vars.prevLevel && current.level > 0) { | 178 | if (vars.firstRoom |
179 | && current.level != vars.prevLevel | ||
180 | && current.level > 0 | ||
181 | && !vars.noSplitScenes.Contains(vars.Helper.Scenes.Active.Name)) { | ||
182 | print(String.Format("{0}: '{1}'", current.level, vars.Helper.Scenes.Active.Name)); | ||
183 | |||
93 | string action = "NO SPLIT"; | 184 | string action = "NO SPLIT"; |
94 | 185 | ||
95 | // Ignore the split rules when script is reloaded mid-game: | ||
96 | if (vars.prevLevel != 0) { | 186 | if (vars.prevLevel != 0) { |
97 | // Split rules: | 187 | // Split rules: |
98 | if (settings["every"]) { | 188 | if (settings["every"]) { |
99 | if (settings["fall"] || !vars.fall.Contains(current.level)) { | 189 | if (settings["fall"] || !vars.fall.Contains(vars.Helper.Scenes.Active.Name)) { |
100 | action = "SPLIT"; | 190 | action = "SPLIT"; |
101 | } | 191 | } |
102 | } else if (vars.waypoints != null) { | 192 | } else if (vars.waypoints != null) { |
103 | if (vars.waypoints.Contains(current.level)) { | 193 | if (vars.waypoints.Contains(vars.Helper.Scenes.Active.Name)) { |
104 | action = "SPLIT"; | 194 | action = "SPLIT"; |
105 | } | 195 | } |
106 | } | 196 | } |
@@ -115,19 +205,23 @@ split { | |||
115 | print(String.Format("Level changed from {0} to {1}: {2}", vars.prevLevel, current.level, action)); | 205 | print(String.Format("Level changed from {0} to {1}: {2}", vars.prevLevel, current.level, action)); |
116 | } | 206 | } |
117 | 207 | ||
208 | if (vars.endings.Contains(vars.Helper.Scenes.Active.Name)) { | ||
209 | vars.inEnding = true; | ||
210 | } | ||
211 | |||
118 | vars.prevLevel = current.level; | 212 | vars.prevLevel = current.level; |
119 | vars.stopwatch = Stopwatch.StartNew(); | 213 | vars.stopwatch = Stopwatch.StartNew(); |
120 | return action.StartsWith("SPLIT"); | 214 | return action.StartsWith("SPLIT"); |
121 | } else if (!vars.firstRoom && current.level == 9) { | 215 | } else if (!vars.firstRoom && vars.Helper.Scenes.Active.Name == "World_000_Optimized") { |
122 | vars.firstRoom = true; | 216 | vars.firstRoom = true; |
123 | vars.prevLevel = current.level; | 217 | vars.prevLevel = current.level; |
124 | vars.prev.Add(9); | 218 | vars.prev.Add(current.level); |
125 | } | 219 | } |
126 | 220 | ||
127 | // Final split of the game: | 221 | // Final split of the game: |
128 | // Split after being in one of the ending cutscenes for 1.1 seconds. | 222 | // Split after being in one of the ending cutscenes for 1.1 seconds. |
129 | if (settings["raymarchitecture"] | 223 | if (settings["raymarchitecture"] |
130 | && (current.level == 99 || current.level == 103 || current.level == 104) | 224 | && vars.inEnding |
131 | && vars.stopwatch != null | 225 | && vars.stopwatch != null |
132 | && vars.stopwatch.ElapsedMilliseconds >= 1100) { | 226 | && vars.stopwatch.ElapsedMilliseconds >= 1100) { |
133 | print("SPLIT on Raymarchitecture"); | 227 | print("SPLIT on Raymarchitecture"); |
@@ -137,5 +231,15 @@ split { | |||
137 | } | 231 | } |
138 | 232 | ||
139 | reset { | 233 | reset { |
140 | return current.startScreenActive; | 234 | return current.onStartScreen && !old.onStartScreen; |
235 | } | ||
236 | |||
237 | exit | ||
238 | { | ||
239 | vars.Helper.Dispose(); | ||
240 | } | ||
241 | |||
242 | shutdown | ||
243 | { | ||
244 | vars.Helper.Dispose(); | ||
141 | } | 245 | } |