├── .editorconfig
├── .gitignore
├── Directory.Build.props
├── LiveSplit.ScriptableAutoSplit.sln
├── Scripts
├── AnUntitledStory.asl
├── LiveSplit.Darksiders.asl
├── LiveSplit.Minecraft.asl
├── LiveSplit.NecroDancer.asl
├── LiveSplit.Octodad.asl
├── LiveSplit.SuperMario64.asl
├── LiveSplit.YoshisIsland.asl
├── NightSky.asl
└── Ocarina of Time.asl
├── props
├── LiveSplit.ScriptableAutoSplit.Paths.props
└── LiveSplit.ScriptableAutoSplit.props
└── src
└── LiveSplit.ScriptableAutoSplit
├── ASL
├── ASLExceptions.cs
├── ASLGrammar.cs
├── ASLMethod.cs
├── ASLParser.cs
├── ASLScript.cs
├── ASLSettings.cs
└── ASLState.cs
├── Component.cs
├── ComponentSettings.Designer.cs
├── ComponentSettings.cs
├── ComponentSettings.resx
├── Factory.cs
└── LiveSplit.ScriptableAutoSplit.csproj
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # All files
4 | [*]
5 | indent_style = space
6 | trim_trailing_whitespace = true
7 |
8 | # New line preferences
9 | end_of_line = crlf
10 | insert_final_newline = true
11 |
12 | # Xml project files
13 | [*.{csproj,proj}]
14 | indent_size = 2
15 |
16 | # Xml config files
17 | [*.{props,targets}]
18 | indent_size = 2
19 |
20 | # Other markup files
21 | [*.{json,xml,yml}]
22 | indent_size = 2
23 |
24 | # Markdown files
25 | [*.md]
26 | indent_size = 2
27 | trim_trailing_whitespace = false
28 |
29 | # C# files
30 | [*.cs]
31 | indent_size = 4
32 |
33 |
34 | #### C# Style Rules ####
35 | [*.cs]
36 |
37 | file_header_template = unset
38 |
39 | # Uncategorized rules (diagnostics without a property equivalent)
40 |
41 | dotnet_diagnostic.IDE0001.severity = suggestion # Simplify name
42 | dotnet_diagnostic.IDE0002.severity = suggestion # Simplify member access
43 | dotnet_diagnostic.IDE0004.severity = warning # Remove unnecessary cast
44 | dotnet_diagnostic.IDE0005.severity = warning # Remove unnecessary import
45 | dotnet_diagnostic.IDE0035.severity = suggestion # Remove unreachable code
46 | dotnet_diagnostic.IDE0050.severity = warning # Convert anonymous type to tuple
47 | dotnet_diagnostic.IDE0051.severity = suggestion # Remove unused private member
48 | dotnet_diagnostic.IDE0052.severity = suggestion # Remove unread private member
49 | dotnet_diagnostic.IDE0064.severity = suggestion # Make struct fields writable
50 | dotnet_diagnostic.IDE0080.severity = warning # Remove unnecessary suppression operator
51 | dotnet_diagnostic.IDE0082.severity = warning # Convert `typeof` to `nameof`
52 | dotnet_diagnostic.IDE0100.severity = warning # Remove unnecessary equality operator
53 | dotnet_diagnostic.IDE0110.severity = suggestion # Remove unnecessary discard
54 | dotnet_diagnostic.IDE0120.severity = warning # Simplify LINQ expression
55 | dotnet_diagnostic.IDE0280.severity = warning # Use `nameof`
56 |
57 |
58 | #### .NET Coding Conventions ####
59 |
60 | # Organize usings
61 | dotnet_separate_import_directive_groups = true
62 | dotnet_sort_system_directives_first = true
63 |
64 | # this. and Me. preferences
65 | dotnet_style_qualification_for_event = false:warning
66 | dotnet_style_qualification_for_field = false:warning
67 | dotnet_style_qualification_for_method = false:warning
68 | dotnet_style_qualification_for_property = false:warning
69 |
70 | # Language keywords vs BCL types preferences
71 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning
72 | dotnet_style_predefined_type_for_member_access = true:warning
73 |
74 | # Parentheses preferences
75 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
76 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
77 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
78 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
79 |
80 | # Modifier preferences
81 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
82 |
83 | # Expression-level preferences
84 | dotnet_style_coalesce_expression = true:suggestion
85 | dotnet_style_collection_initializer = true:warning
86 | dotnet_style_explicit_tuple_names = true:warning
87 | dotnet_style_null_propagation = true:suggestion
88 | dotnet_style_object_initializer = true:warning
89 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
90 | dotnet_style_prefer_auto_properties = true:warning
91 | dotnet_style_prefer_collection_expression = true:suggestion
92 | dotnet_style_prefer_compound_assignment = true:warning
93 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
94 | dotnet_style_prefer_conditional_expression_over_return = false:silent
95 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
96 | dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
97 | dotnet_style_prefer_inferred_tuple_names = false:suggestion
98 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
99 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
100 | dotnet_style_prefer_simplified_interpolation = true:suggestion
101 | csharp_style_throw_expression = false:warning
102 |
103 | # Field preferences
104 | dotnet_style_readonly_field = true:warning
105 |
106 | # Parameter preferences
107 | dotnet_code_quality_unused_parameters = all:suggestion
108 |
109 | # Suppression preferences
110 | dotnet_remove_unnecessary_suppression_exclusions = suggestion
111 |
112 | # New line preferences
113 | dotnet_style_allow_multiple_blank_lines_experimental = false:warning
114 | dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
115 |
116 |
117 | #### C# Coding Conventions ####
118 |
119 | # readonly preferences
120 | csharp_style_prefer_readonly_struct = true:warning
121 | csharp_style_prefer_readonly_struct_member = true:warning
122 |
123 | # var preferences
124 | csharp_style_var_elsewhere = false:suggestion
125 | csharp_style_var_for_built_in_types = false:suggestion
126 | csharp_style_var_when_type_is_apparent = true:suggestion
127 |
128 | # Expression-bodied members
129 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
130 | csharp_style_expression_bodied_constructors = false:warning
131 | csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
132 | csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
133 | csharp_style_expression_bodied_local_functions = false:warning
134 | csharp_style_expression_bodied_methods = false:warning
135 | csharp_style_expression_bodied_operators = false:warning
136 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion
137 |
138 | # Pattern matching preferences
139 | csharp_style_pattern_matching_over_as_with_null_check = true:warning
140 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
141 | csharp_style_prefer_not_pattern = true:warning
142 | csharp_style_prefer_extended_property_pattern = true:warning
143 | csharp_style_prefer_pattern_matching = true:suggestion
144 | csharp_style_prefer_switch_expression = true:suggestion
145 |
146 | # Null-checking preferences
147 | csharp_style_conditional_delegate_call = true:suggestion
148 |
149 | # Modifier preferences
150 | csharp_prefer_static_local_function = true:warning
151 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
152 |
153 | # Code-block preferences
154 | csharp_prefer_braces = true:warning
155 | csharp_prefer_simple_using_statement = true:warning
156 | csharp_style_prefer_method_group_conversion = true:suggestion
157 | csharp_style_prefer_top_level_statements = true:suggestion
158 |
159 | # Expression-level preferences
160 | csharp_prefer_simple_default_expression = true:warning
161 | csharp_style_deconstructed_variable_declaration = true:warning
162 | csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
163 | csharp_style_inlined_variable_declaration = true:warning
164 | csharp_style_pattern_local_over_anonymous_function = true:warning
165 | csharp_style_prefer_index_operator = true:warning
166 | csharp_style_prefer_local_over_anonymous_function = true:warning
167 | csharp_style_prefer_null_check_over_type_check = true:warning
168 | csharp_style_prefer_range_operator = true:warning
169 | csharp_style_prefer_tuple_swap = true:warning
170 | csharp_style_prefer_utf8_string_literals = true:suggestion
171 | csharp_style_unused_value_assignment_preference = discard_variable:none
172 | csharp_style_unused_value_expression_statement_preference = discard_variable:none
173 |
174 | # 'using' directive preferences
175 | csharp_using_directive_placement = outside_namespace:warning
176 |
177 |
178 | #### C# Formatting Rules ####
179 |
180 | # New line preferences
181 | csharp_new_line_before_catch = true
182 | csharp_new_line_before_else = true
183 | csharp_new_line_before_finally = true
184 | csharp_new_line_before_members_in_anonymous_types = true
185 | csharp_new_line_before_members_in_object_initializers = true
186 | csharp_new_line_before_open_brace = all
187 | csharp_new_line_between_query_expression_clauses = true
188 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:warning
189 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:warning
190 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:warning
191 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
192 | csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning
193 |
194 | # Indentation preferences
195 | csharp_indent_block_contents = true
196 | csharp_indent_braces = false
197 | csharp_indent_case_contents = true
198 | csharp_indent_case_contents_when_block = false
199 | csharp_indent_labels = one_less_than_current
200 | csharp_indent_switch_labels = true
201 |
202 | # Space preferences
203 | csharp_space_after_cast = false
204 | csharp_space_after_colon_in_inheritance_clause = true
205 | csharp_space_after_comma = true
206 | csharp_space_after_dot = false
207 | csharp_space_after_keywords_in_control_flow_statements = true
208 | csharp_space_after_semicolon_in_for_statement = true
209 | csharp_space_around_binary_operators = before_and_after
210 | csharp_space_around_declaration_statements = false
211 | csharp_space_before_colon_in_inheritance_clause = true
212 | csharp_space_before_comma = false
213 | csharp_space_before_dot = false
214 | csharp_space_before_open_square_brackets = false
215 | csharp_space_before_semicolon_in_for_statement = false
216 | csharp_space_between_empty_square_brackets = false
217 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
218 | csharp_space_between_method_call_name_and_opening_parenthesis = false
219 | csharp_space_between_method_call_parameter_list_parentheses = false
220 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
221 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
222 | csharp_space_between_method_declaration_parameter_list_parentheses = false
223 | csharp_space_between_parentheses = false
224 | csharp_space_between_square_brackets = false
225 |
226 | # Wrapping preferences
227 | csharp_preserve_single_line_blocks = true
228 | csharp_preserve_single_line_statements = true
229 |
230 | # Namespace preferences
231 | csharp_style_namespace_declarations = file_scoped:warning
232 | dotnet_style_namespace_match_folder = false
233 |
234 | # Constructor preferences
235 | csharp_style_prefer_primary_constructors = false:none
236 |
237 |
238 | #### Naming styles ####
239 |
240 | # Naming rules
241 |
242 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
243 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
244 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
245 |
246 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
247 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
248 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
249 |
250 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
251 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
252 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
253 |
254 | dotnet_naming_rule.constant_fields_should_be_pascalcase.severity = suggestion
255 | dotnet_naming_rule.constant_fields_should_be_pascalcase.symbols = constant_fields
256 | dotnet_naming_rule.constant_fields_should_be_pascalcase.style = pascalcase
257 |
258 | dotnet_naming_rule.public_static_fields_should_be_pascalcase.severity = suggestion
259 | dotnet_naming_rule.public_static_fields_should_be_pascalcase.symbols = public_static_fields
260 | dotnet_naming_rule.public_static_fields_should_be_pascalcase.style = pascalcase
261 |
262 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
263 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
264 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
265 |
266 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
267 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
268 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
269 |
270 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = warning
271 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
272 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
273 |
274 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
275 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
276 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
277 |
278 | dotnet_naming_rule.local_constants_should_be_pascalcase.severity = warning
279 | dotnet_naming_rule.local_constants_should_be_pascalcase.symbols = local_constants
280 | dotnet_naming_rule.local_constants_should_be_pascalcase.style = pascalcase
281 |
282 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = warning
283 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
284 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
285 |
286 | dotnet_naming_rule.local_functions_should_be_camelcase.severity = warning
287 | dotnet_naming_rule.local_functions_should_be_camelcase.symbols = local_functions
288 | dotnet_naming_rule.local_functions_should_be_camelcase.style = camelcase
289 |
290 | # Symbol specifications
291 |
292 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
293 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
294 | dotnet_naming_symbols.types_and_namespaces.required_modifiers =
295 |
296 | dotnet_naming_symbols.interfaces.applicable_kinds = interface
297 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
298 | dotnet_naming_symbols.interfaces.required_modifiers =
299 |
300 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, delegate, method
301 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
302 | dotnet_naming_symbols.non_field_members.required_modifiers =
303 |
304 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
305 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
306 | dotnet_naming_symbols.constant_fields.required_modifiers = const
307 |
308 | dotnet_naming_symbols.public_static_fields.applicable_kinds = field
309 | dotnet_naming_symbols.public_static_fields.applicable_accessibilities = public, internal
310 | dotnet_naming_symbols.public_static_fields.required_modifiers = static
311 |
312 | dotnet_naming_symbols.public_fields.applicable_kinds = field
313 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
314 | dotnet_naming_symbols.public_fields.required_modifiers =
315 |
316 | dotnet_naming_symbols.private_fields.applicable_kinds = field
317 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
318 | dotnet_naming_symbols.private_fields.required_modifiers =
319 |
320 | dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter
321 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
322 | dotnet_naming_symbols.type_parameters.required_modifiers =
323 |
324 | dotnet_naming_symbols.parameters.applicable_kinds = parameter
325 | dotnet_naming_symbols.parameters.applicable_accessibilities = *
326 | dotnet_naming_symbols.parameters.required_modifiers =
327 |
328 | dotnet_naming_symbols.local_constants.applicable_kinds = local
329 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local
330 | dotnet_naming_symbols.local_constants.required_modifiers = const
331 |
332 | dotnet_naming_symbols.local_variables.applicable_kinds = local
333 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local
334 | dotnet_naming_symbols.local_variables.required_modifiers =
335 |
336 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function
337 | dotnet_naming_symbols.local_functions.applicable_accessibilities = local
338 | dotnet_naming_symbols.local_functions.required_modifiers =
339 |
340 | # Naming styles
341 |
342 | dotnet_naming_style.pascalcase.required_prefix =
343 | dotnet_naming_style.pascalcase.required_suffix =
344 | dotnet_naming_style.pascalcase.word_separator =
345 | dotnet_naming_style.pascalcase.capitalization = pascal_case
346 |
347 | dotnet_naming_style.ipascalcase.required_prefix = I
348 | dotnet_naming_style.ipascalcase.required_suffix =
349 | dotnet_naming_style.ipascalcase.word_separator =
350 | dotnet_naming_style.ipascalcase.capitalization = pascal_case
351 |
352 | dotnet_naming_style.tpascalcase.required_prefix = T
353 | dotnet_naming_style.tpascalcase.required_suffix =
354 | dotnet_naming_style.tpascalcase.word_separator =
355 | dotnet_naming_style.tpascalcase.capitalization = pascal_case
356 |
357 | dotnet_naming_style.camelcase.required_prefix =
358 | dotnet_naming_style.camelcase.required_suffix =
359 | dotnet_naming_style.camelcase.word_separator =
360 | dotnet_naming_style.camelcase.capitalization = camel_case
361 |
362 | dotnet_naming_style._camelcase.required_prefix = _
363 | dotnet_naming_style._camelcase.required_suffix =
364 | dotnet_naming_style._camelcase.word_separator =
365 | dotnet_naming_style._camelcase.capitalization = camel_case
366 |
367 |
368 | #### Suppressions ####
369 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE options/cache directories
2 | .vs/
3 | .vscode/
4 | .idea/
5 | .fleet/
6 |
7 | # Build artifacts
8 | artifacts/
9 | bin/
10 | obj/
11 | publish/
12 |
13 | # User-specific files
14 | *.user
15 |
16 | # MSBuild
17 | *.binlog
18 |
19 | # Operating System Files
20 | # =========================
21 |
22 | # OSX
23 | .DS_Store
24 | .AppleDouble
25 | .LSOverride
26 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/LiveSplit.ScriptableAutoSplit.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2EB7974C-9640-4AE5-AEB4-C225962995C6}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveSplit.ScriptableAutoSplit", "src\LiveSplit.ScriptableAutoSplit\LiveSplit.ScriptableAutoSplit.csproj", "{833B746E-DAFC-4D53-BC53-BE3FDC0796ED}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(SolutionProperties) = preSolution
16 | HideSolutionNode = FALSE
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {833B746E-DAFC-4D53-BC53-BE3FDC0796ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {833B746E-DAFC-4D53-BC53-BE3FDC0796ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {833B746E-DAFC-4D53-BC53-BE3FDC0796ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {833B746E-DAFC-4D53-BC53-BE3FDC0796ED}.Release|Any CPU.Build.0 = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(NestedProjects) = preSolution
25 | {833B746E-DAFC-4D53-BC53-BE3FDC0796ED} = {2EB7974C-9640-4AE5-AEB4-C225962995C6}
26 | EndGlobalSection
27 | EndGlobal
28 |
--------------------------------------------------------------------------------
/Scripts/AnUntitledStory.asl:
--------------------------------------------------------------------------------
1 | state("Anuntitledstory")
2 | {
3 | double time : 0x189720, 0x4, 0x358;
4 | }
5 |
6 | start
7 | {
8 | return current.time == 1 && old.time == 0;
9 | }
10 |
11 | reset
12 | {
13 | return old.time > current.time;
14 | }
15 |
16 | isLoading
17 | {
18 | return false;
19 | }
20 |
21 | gameTime
22 | {
23 | return TimeSpan.FromSeconds(current.time);
24 | }
25 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.Darksiders.asl:
--------------------------------------------------------------------------------
1 | state("DarksidersPC")
2 | {
3 | int time : 0x122E594, 0x53C, 0x74;
4 | }
5 |
6 | start
7 | {
8 | return current.time == 1;
9 | }
10 |
11 | isLoading
12 | {
13 | return true;
14 | }
15 |
16 | gameTime
17 | {
18 | return TimeSpan.FromSeconds(current.time);
19 | }
20 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.Minecraft.asl:
--------------------------------------------------------------------------------
1 | state("javaw")
2 | {
3 | }
4 |
5 | start
6 | {
7 | vars.startTime = -1;
8 | vars.gameTime = -1;
9 | vars.realTime = TimeSpan.Zero;
10 | vars.realTimeDelta = TimeSpan.Zero;
11 | }
12 |
13 | isLoading
14 | {
15 | return vars.realTimeDelta.TotalSeconds < 4.5;
16 | }
17 |
18 | gameTime
19 | {
20 | var path = System.IO.Path.Combine(Environment.GetEnvironmentVariable("appdata"), ".minecraft\\stats");
21 | var source = System.IO.Directory.EnumerateFiles(path, "stats_*_unsent.dat");
22 | if (source.Any())
23 | {
24 | var lines = System.IO.File.ReadAllLines(source.First());
25 | foreach (var line in lines)
26 | {
27 | var trimmed = line.Trim();
28 | if (trimmed.StartsWith("{\"1100\":"))
29 | {
30 | var value = trimmed.Substring(8);
31 | var num = 0;
32 | for (int i = 0; i < value.Length; i++)
33 | {
34 | char c = value[i];
35 | if (!char.IsDigit(c))
36 | {
37 | break;
38 | }
39 | num = 10 * num + (int)(c - '0');
40 | }
41 | if (vars.startTime == -1)
42 | {
43 | vars.startTime = num;
44 | }
45 | var prevGameTime = vars.gameTime;
46 | vars.gameTime = (num - vars.startTime) * 50;
47 | if (vars.gameTime != prevGameTime)
48 | {
49 | var prevRealTime = vars.realTime;
50 | vars.realTime = timer.CurrentTime.RealTime;
51 | vars.realTimeDelta = vars.realTime - prevRealTime;
52 | return new TimeSpan(0, 0, 0, 0, vars.gameTime);
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.NecroDancer.asl:
--------------------------------------------------------------------------------
1 | state("NecroDancer")
2 | {
3 | int ZoneID : 0x06DA40, 0x230;
4 | int LevelID : 0x06DA40, 0x2C4;
5 | int LevelTime : 0x06DA40, 0x10C;
6 | int Health : 0x18EF38, 0x014, 0x130;
7 | byte GamePaused : 0x06DA40, 0x088;
8 | }
9 |
10 | start
11 | {
12 | vars.GameTime = 0;
13 | vars.AccumulatedTime = 0;//-current.LevelTime;
14 |
15 | var isNotInMainRoom = !(current.ZoneID == 1 && current.LevelID == -2);
16 | var isInANewLevel = (old.ZoneID != current.ZoneID
17 | || old.LevelID != current.LevelID);
18 | var isRevived = old.Health <= 0 && current.Health > 0;
19 |
20 | return isNotInMainRoom && (isInANewLevel || isRevived);
21 | }
22 |
23 | split
24 | {
25 | return old.ZoneID != current.ZoneID
26 | || old.LevelID != current.LevelID;
27 | }
28 |
29 | reset
30 | {
31 | return current.Health <= 0
32 | || (current.ZoneID == 1 && current.LevelID == -2);
33 | }
34 |
35 | isLoading
36 | {
37 | return false;
38 | }
39 |
40 | gameTime
41 | {
42 | if (current.LevelTime < old.LevelTime
43 | && !(current.ZoneID == 1 && current.LevelID == 1))
44 | current.AccumulatedTime += old.LevelTime;
45 |
46 | var prevGameTime = vars.GameTime;
47 | vars.GameTime = current.LevelTime + current.AccumulatedTime;
48 | if (vars.GameTime != prevGameTime || current.GamePaused != 0)
49 | return TimeSpan.FromMilliseconds(vars.GameTime);
50 | }
51 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.Octodad.asl:
--------------------------------------------------------------------------------
1 | state("OctodadDadliestCatch")
2 | {
3 | string48 levelName : 0x23FD88, 0xd8, 0x0C, 0x4C, 0x0;
4 | bool isLoading : 0x23FD8C, 0x4C, 0x8C;
5 | float levelTimer : 0x23FD88, 0xD8, 0x08;
6 | }
7 |
8 | start
9 | {
10 | return (current.levelName == "" || current.levelName.StartsWith("MainScreen_Background"))
11 | && current.isLoading && !old.isLoading;
12 | }
13 |
14 | split
15 | {
16 | return !old.isLoading && current.isLoading
17 | && !old.levelName.StartsWith("MainScreen_Background")
18 | && !old.levelName.StartsWith("OpeningCredits")
19 | && !(timer.CurrentSplitIndex >= 5 && timer.CurrentSplitIndex <= 7 && old.levelName.StartsWith("Aquarium_Hub"));
20 | }
21 |
22 | isLoading
23 | {
24 | return current.isLoading;
25 | }
26 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.SuperMario64.asl:
--------------------------------------------------------------------------------
1 | state("project64")
2 | {
3 | byte Stars : 0xD6A1C, 0x33B218;
4 | int GameFrames : 0xD6A1C, 0x32D5D4;
5 | }
6 |
7 | start
8 | {
9 | vars.AccumulatedFrames = 0;
10 | vars.StarTime = 0;
11 |
12 | return old.GameFrames > 0 && current.GameFrames == 0;
13 | }
14 |
15 | split
16 | {
17 | if (old.Stars < current.Stars)
18 | vars.StarTime = current.GameFrames;
19 |
20 | if (vars.StarTime > 0)
21 | {
22 | var delta = current.GameFrames - vars.StarTime;
23 |
24 | if (delta > 120)
25 | {
26 | vars.StarTime = 0;
27 | return true;
28 | }
29 | }
30 | }
31 |
32 | isLoading
33 | {
34 | return false;
35 | }
36 |
37 | gameTime
38 | {
39 | if (current.GameFrames < old.GameFrames)
40 | vars.AccumulatedFrames += old.GameFrames;
41 |
42 | return TimeSpan.FromSeconds((current.GameFrames + vars.AccumulatedFrames) / 30.0f);
43 | }
44 |
--------------------------------------------------------------------------------
/Scripts/LiveSplit.YoshisIsland.asl:
--------------------------------------------------------------------------------
1 | state("snes9x")
2 | {
3 | byte isInALevel : 0x2EFBA4, 0x2904A;
4 | short levelFrames : 0x2EFBA4, 0x003A9;
5 | }
6 |
7 | start
8 | {
9 | vars.gameTime = TimeSpan.Zero;
10 | return old.isInALevel == 4 && current.isInALevel == 0;
11 | }
12 |
13 | split
14 | {
15 | if (current.levelFrames < old.levelFrames)
16 | vars.gameTime += TimeSpan.FromSeconds(old.levelFrames / 60.0f);
17 |
18 | return old.isInALevel == 1 && current.isInALevel == 0;
19 | }
20 |
21 | isLoading
22 | {
23 | return true;
24 | }
25 |
26 | gameTime
27 | {
28 | return vars.gameTime + TimeSpan.FromSeconds(current.levelFrames / 60.0f);
29 | }
30 |
--------------------------------------------------------------------------------
/Scripts/NightSky.asl:
--------------------------------------------------------------------------------
1 | state("NightSky")
2 | {
3 | bool isLoading : 0x211490, 0x498, 0x035;
4 | int chapterID : 0x211490, 0x0A4, 0x1B0;
5 | }
6 |
7 | start
8 | {
9 | return old.chapterID == 20239 && current.chapterID == 0;
10 | }
11 |
12 | split
13 | {
14 | return !old.isLoading && current.isLoading && current.chapterID != 0;
15 | }
16 |
17 | isLoading
18 | {
19 | return current.isLoading;
20 | }
21 |
--------------------------------------------------------------------------------
/Scripts/Ocarina of Time.asl:
--------------------------------------------------------------------------------
1 | state("project64")
2 | {
3 | //Addresses in Little Endian instead of Big Endian
4 | byte400 Data : 0xD6A1C, 0x11A570;
5 | int GameFrames : 0xD6A1C, 0x11F568;
6 | byte SceneID : 0xD6A1C, 0x1C8546;
7 | sbyte GohmasHealth : 0xD6A1C, 0x1E840C;
8 | sbyte GanonsHealth : 0xD6A1C, 0x1FA2DC;
9 | short GanonsAnimation : 0xD6A1C, 0x1FA374;
10 | short DialogID : 0xD6A1C, 0x1D8872;
11 | byte IsOnTitleScreenOrFileSelect : 0xD6A1C, 0x11B92C;
12 | float X : 0xD6A1C, 0x1C8714;
13 | float Y : 0xD6A1C, 0x1C8718;
14 | float Z : 0xD6A1C, 0x1C871C;
15 | }
16 |
17 | start
18 | {
19 | //Set Start Up State
20 | current.GameTime = TimeSpan.Zero;
21 | current.AccumulatedFrames = -current.GameFrames;
22 |
23 | current.EntranceID = 0xFFFF;
24 | current.CutsceneID = 0x0;
25 |
26 | current.EyeBallFrogCount =
27 | current.LastActualDialog =
28 | current.LastActualDialogTime =
29 | 0;
30 |
31 | current.HasSword =
32 | current.HasBottle =
33 | current.HasIceArrows =
34 | current.HasFaroresWind =
35 | current.HasSlingshot =
36 | current.HasBombchus =
37 | current.HasHookshot =
38 | current.HasIronBoots =
39 | current.HasHoverBoots =
40 | current.HasSongOfStorms =
41 | current.HasBombs =
42 | current.HasBoleroOfFire =
43 | current.HasRequiemOfSpirit =
44 | current.HasEyeBallFrog =
45 | current.HasMasterSword =
46 | current.HasBiggoronSword =
47 | current.HasBow =
48 | current.IsInFairyOcarinaCutscene =
49 | current.DidMidoSkip =
50 | false;
51 |
52 | //Check for Timer Start
53 | return (old.SceneID == 0x17 || old.SceneID == 0x18)
54 | && !(current.SceneID == 0x17 || current.SceneID == 0x18);
55 | }
56 |
57 | split
58 | {
59 | //Functions
60 | Func check = (x, y) => (x & y) != 0x0;
61 | Func readFixed = x => current.Data[x ^ 0x3];
62 | Func getInventoryItem = slot => readFixed(0xD4 + slot);
63 | Func getEntranceID = () => (short)(current.Data[0x61] << 8 | current.Data[0x60]);
64 | Func getCutsceneID = () => (ushort)(current.Data[0x69] << 8 | current.Data[0x68]);
65 | Func checkEntrance = (x, y) => (x | 0x3) == (y | 0x3);
66 |
67 | //General Constants
68 | const byte INVENTORY_WIDTH = 6;
69 |
70 | //Flags
71 | const byte HAS_KOKIRI_SWORD = 0x1;
72 | const byte HAS_MASTER_SWORD = 0x2;
73 | const byte HAS_BIGGORON_SWORD = 0x4;
74 | const byte HAS_IRON_BOOTS = 0x20;
75 | const byte HAS_HOVER_BOOTS = 0x40;
76 | const byte HAS_REQUIEM_OF_SPIRIT = 0x2;
77 | const byte HAS_SONG_OF_STORMS = 0x2;
78 | const byte HAS_BOLERO_OF_FIRE = 0x80;
79 |
80 | const byte IS_ON_TITLE_SCREEN = 0x1;
81 | const byte IS_ON_FILE_SELECT = 0x2;
82 |
83 | //Scene IDs
84 | const byte DEKU_TREE = 0x00;
85 | const byte KAKARIKO = 0x52;
86 | const byte KOKIRI_FOREST = 0x55;
87 | const byte GOHMA = 0x11;
88 |
89 | //Entrance IDs
90 | //const short DEKU_TREE = 0x0;
91 | const short FOREST_TO_RIVER = 0x1DD;
92 | const short BRIDGE_BETWEEN_FIELD_AND_FOREST = 0x011E;
93 | const short WRONG_WARP_ENTRANCE = 0x256;
94 | const short VOLVAGIA_BATTLE = 0x305;
95 | const short GANONDORF_DEAD = 0x330;
96 | //const short GOHMA = 0x40F;
97 | const short GANON_BATTLE = 0x517;
98 | const short DODONGO_BATTLE = 0x40B;
99 |
100 | //Item IDs
101 | const byte BOMBS = 0x02;
102 | const byte BOW = 0x03;
103 | const byte SLINGSHOT = 0x06;
104 | const byte BOMBCHUS = 0x09;
105 | const byte HOOKSHOT = 0x0A;
106 | const byte ICE_ARROWS = 0x0C;
107 | const byte FARORES_WIND = 0x0D;
108 | const byte EMPTY_BOTTLE = 0x14;
109 | const byte EYE_BALL_FROG = 0x35;
110 |
111 | //Inventory Slots
112 | const byte BOMBS_SLOT = 2 + 0 * INVENTORY_WIDTH;
113 | const byte BOW_SLOT = 3 + 0 * INVENTORY_WIDTH;
114 | const byte SLINGSHOT_SLOT = 0 + 1 * INVENTORY_WIDTH;
115 | const byte OCARINA_SLOT = 1 + 1 * INVENTORY_WIDTH;
116 | const byte BOMBCHUS_SLOT = 2 + 1 * INVENTORY_WIDTH;
117 | const byte HOOKSHOT_SLOT = 3 + 1 * INVENTORY_WIDTH;
118 | const byte ICE_ARROWS_SLOT = 4 + 1 * INVENTORY_WIDTH;
119 | const byte FARORES_WIND_SLOT = 5 + 1 * INVENTORY_WIDTH;
120 | const byte BOTTLE_1 = 0 + 3 * INVENTORY_WIDTH;
121 | const byte BOTTLE_2 = 1 + 3 * INVENTORY_WIDTH;
122 | const byte BOTTLE_3 = 2 + 3 * INVENTORY_WIDTH;
123 | const byte BOTTLE_4 = 3 + 3 * INVENTORY_WIDTH;
124 | const byte ADULT_TRADE_ITEM = 4 + 3 * INVENTORY_WIDTH;
125 | const byte CHILD_TRADE_ITEM = 5 + 3 * INVENTORY_WIDTH;
126 |
127 | //Dialog IDs
128 | const short NO_DIALOG = 0x0000;
129 | const short FARORES_WIND_DIALOG = 0x003B;
130 | const short REQUIEM_OF_SPIRIT_DIALOG = 0x0076;
131 | const short SONG_OF_STORMS_DIALOG = 0x00D6;
132 |
133 | //Animation IDs
134 | const short GANON_FINAL_HIT = 0x3B1C;
135 |
136 | //Cutscene IDs
137 | const ushort FAIRY_OCARINA_CUTSCENE = 0xFFF0;
138 |
139 | //Check for Split
140 | var segment = timer.CurrentSplit.Name.ToLower();
141 |
142 | //Don't split on Title Screen or File Select because
143 | //Title Screen Link and Third File Link are loaded
144 | //and they might cause some splits to happen
145 | if (current.IsOnTitleScreenOrFileSelect != 0x0)
146 | {
147 | //TODO Except some segments
148 | return false;
149 | }
150 |
151 | if (segment == "sword" || segment == "kokiri sword")
152 | {
153 | var swordsAndShieldsUnlocked = current.Data[0xFE];
154 | current.HasSword = check(swordsAndShieldsUnlocked, HAS_KOKIRI_SWORD);
155 | return !old.HasSword && current.HasSword;
156 | }
157 | else if (segment == "master sword")
158 | {
159 | var swordsAndShieldsUnlocked = current.Data[0xFE];
160 | current.HasMasterSword = check(swordsAndShieldsUnlocked, HAS_MASTER_SWORD);
161 | return !old.HasMasterSword && current.HasMasterSword;
162 | }
163 | else if (segment == "biggoron sword" || segment == "biggoron's sword")
164 | {
165 | var swordsAndShieldsUnlocked = current.Data[0xFE];
166 | current.HasBiggoronSword = check(swordsAndShieldsUnlocked, HAS_BIGGORON_SWORD);
167 | return !old.HasBiggoronSword && current.HasBiggoronSword;
168 | }
169 | else if (segment == "hover boots")
170 | {
171 | var bootsAndTunicsUnlocked = current.Data[0xFF];
172 | current.HasHoverBoots = check(bootsAndTunicsUnlocked, HAS_HOVER_BOOTS);
173 | return !old.HasHoverBoots && current.HasHoverBoots;
174 | }
175 | else if (segment == "iron boots")
176 | {
177 | var bootsAndTunicsUnlocked = current.Data[0xFF];
178 | current.HasIronBoots = check(bootsAndTunicsUnlocked, HAS_IRON_BOOTS);
179 | return !old.HasIronBoots && current.HasIronBoots;
180 | }
181 | else if (segment == "song of storms")
182 | {
183 | var songsAndEmeraldsUnlocked = current.Data[0x106];
184 | current.HasSongOfStorms = check(songsAndEmeraldsUnlocked, HAS_SONG_OF_STORMS);
185 | return old.DialogID == SONG_OF_STORMS_DIALOG
186 | && current.DialogID == NO_DIALOG
187 | && current.HasSongOfStorms;
188 | }
189 | else if (segment == "bolero of fire")
190 | {
191 | var songsAndMedallionsUnlocked = current.Data[0x104];
192 | current.HasBoleroOfFire = check(songsAndMedallionsUnlocked, HAS_BOLERO_OF_FIRE);
193 | return !old.HasBoleroOfFire && current.HasBoleroOfFire;
194 | }
195 | else if (segment == "requiem of spirit")
196 | {
197 | var songsUnlocked = current.Data[0x105];
198 | current.HasRequiemOfSpirit = check(songsUnlocked, HAS_REQUIEM_OF_SPIRIT);
199 | return old.DialogID == REQUIEM_OF_SPIRIT_DIALOG
200 | && current.DialogID == NO_DIALOG
201 | && current.HasRequiemOfSpirit;
202 | }
203 | else if (segment == "bottle")
204 | {
205 | current.HasBottle = getInventoryItem(BOTTLE_1) == EMPTY_BOTTLE;
206 | return !old.HasBottle && current.HasBottle;
207 | }
208 | else if (segment.StartsWith("ice arrow"))
209 | {
210 | current.HasIceArrows = getInventoryItem(ICE_ARROWS_SLOT) == ICE_ARROWS;
211 | return !old.HasIceArrows && current.HasIceArrows;
212 | }
213 | else if (segment.StartsWith("farore's wind"))
214 | {
215 | current.HasFaroresWind = getInventoryItem(FARORES_WIND_SLOT) == FARORES_WIND;
216 | return !old.HasFaroresWind && current.HasFaroresWind;
217 | }
218 | else if (segment.EndsWith("bow"))
219 | {
220 | current.HasBow = getInventoryItem(BOW_SLOT) == BOW;
221 | return !old.HasBow && current.HasBow;
222 | //TODO Test
223 | }
224 | else if (segment.EndsWith("frog"))
225 | {
226 | current.HasEyeBallFrog = getInventoryItem(ADULT_TRADE_ITEM) == EYE_BALL_FROG;
227 | if (!old.HasEyeBallFrog && current.HasEyeBallFrog)
228 | current.EyeBallFrogCount++;
229 | return current.EyeBallFrogCount == 2 && old.EyeBallFrogCount < 2;
230 | }
231 | else if (segment == "slingshot")
232 | {
233 | current.HasSlingshot = getInventoryItem(SLINGSHOT_SLOT) == SLINGSHOT;
234 | return !old.HasSlingshot && current.HasSlingshot;
235 | }
236 | else if (segment == "bombchus")
237 | {
238 | current.HasBombchus = getInventoryItem(BOMBCHUS_SLOT) == BOMBCHUS;
239 | return !old.HasBombchus && current.HasBombchus;
240 | }
241 | else if (segment == "hookshot")
242 | {
243 | current.HasHookshot = getInventoryItem(HOOKSHOT_SLOT) == HOOKSHOT;
244 | return !old.HasHookshot && current.HasHookshot;
245 | }
246 | else if (segment == "bombs")
247 | {
248 | current.HasBombs = getInventoryItem(BOMBS_SLOT) == BOMBS;
249 | return !old.HasBombs && current.HasBombs;
250 | }
251 | else if (segment == "forest escape" || segment == "escape")
252 | {
253 | current.EntranceID = getEntranceID();
254 | current.CutsceneID = getCutsceneID();
255 |
256 | var escapedToRiver =
257 | !checkEntrance(old.EntranceID, FOREST_TO_RIVER)
258 | && checkEntrance(current.EntranceID, FOREST_TO_RIVER);
259 |
260 | current.IsInFairyOcarinaCutscene =
261 | checkEntrance(current.EntranceID, BRIDGE_BETWEEN_FIELD_AND_FOREST)
262 | && current.CutsceneID == FAIRY_OCARINA_CUTSCENE;
263 |
264 | var escapedToSaria =
265 | !old.IsInFairyOcarinaCutscene
266 | && current.IsInFairyOcarinaCutscene;
267 |
268 | return escapedToRiver || escapedToSaria;
269 | }
270 | else if (segment == "kakariko")
271 | {
272 | return old.SceneID != KAKARIKO && current.SceneID == KAKARIKO;
273 | }
274 | else if (segment == "mido skip")
275 | {
276 | current.DidMidoSkip =
277 | current.SceneID == KOKIRI_FOREST
278 | && current.X > 1600
279 | && current.Y >= 0;
280 |
281 | return !old.DidMidoSkip && current.DidMidoSkip;
282 | }
283 | else if (segment == "deku tree")
284 | {
285 | return old.SceneID == KOKIRI_FOREST && current.SceneID == DEKU_TREE;
286 | }
287 | else if (segment == "gohma")
288 | {
289 | return current.SceneID == GOHMA
290 | && old.GohmasHealth > 0
291 | && current.GohmasHealth <= 0;
292 | }
293 | else if (segment == "ganondorf" || segment == "wrong warp")
294 | {
295 | current.EntranceID = getEntranceID();
296 | return checkEntrance(old.EntranceID, WRONG_WARP_ENTRANCE)
297 | && !checkEntrance(current.EntranceID, WRONG_WARP_ENTRANCE);
298 | }
299 | else if (segment == "fire temple")
300 | {
301 | current.EntranceID = getEntranceID();
302 | return checkEntrance(old.EntranceID, VOLVAGIA_BATTLE)
303 | && !checkEntrance(current.EntranceID, VOLVAGIA_BATTLE);
304 | //TODO Test with wrong warp
305 | }
306 | else if (segment == "collapse" || segment == "tower collapse")
307 | {
308 | current.EntranceID = getEntranceID();
309 | return !checkEntrance(old.EntranceID, GANON_BATTLE)
310 | && checkEntrance(current.EntranceID, GANON_BATTLE);
311 | }
312 | else if (segment.EndsWith("warp in fire"))
313 | {
314 | if (current.DialogID != NO_DIALOG)
315 | {
316 | current.LastActualDialog = current.DialogID;
317 | current.LastActualDialogTime = current.GameFrames;
318 | }
319 |
320 | if (current.LastActualDialog == FARORES_WIND_DIALOG)
321 | {
322 | var delta = current.GameFrames - current.LastActualDialogTime;
323 |
324 | if (delta >= 30)
325 | {
326 | current.LastActualDialog = NO_DIALOG;
327 | }
328 |
329 | return current.X == 0
330 | && current.Y == 0
331 | && current.Z == 0;
332 | }
333 | }
334 | else if (segment.EndsWith("dodongo hc") || segment.EndsWith("dodongo heart container"))
335 | {
336 | current.EntranceID = getEntranceID();
337 | current.HeartContainers = current.Data[0x8c] >> 4;
338 | return checkEntrance(current.EntranceID, DODONGO_BATTLE)
339 | && current.HeartContainers > old.HeartContainers;
340 | }
341 | else if (segment == "ganon")
342 | {
343 | current.EntranceID = getEntranceID();
344 | return checkEntrance(current.EntranceID, GANON_BATTLE)
345 | && current.GanonsHealth <= 0
346 | && old.GanonsAnimation != GANON_FINAL_HIT
347 | && current.GanonsAnimation == GANON_FINAL_HIT;
348 | }
349 | }
350 |
351 | isLoading
352 | {
353 | return true;
354 | }
355 |
356 | gameTime
357 | {
358 | if (current.GameFrames < old.GameFrames)
359 | current.AccumulatedFrames += old.GameFrames;
360 |
361 | return TimeSpan.FromSeconds((current.GameFrames + current.AccumulatedFrames) / 20.0f);
362 | }
363 |
--------------------------------------------------------------------------------
/props/LiveSplit.ScriptableAutoSplit.Paths.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildThisFileDirectory)..
5 |
6 | $(RootPath)\src
7 | $(RootPath)\test
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/props/LiveSplit.ScriptableAutoSplit.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 12
6 |
7 | true
8 | disable
9 |
10 |
11 |
12 |
13 | true
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLExceptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.CodeDom.Compiler;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace LiveSplit.ASL;
8 |
9 | public class ASLCompilerException : Exception
10 | {
11 | public ASLMethod Method { get; }
12 |
13 | public CompilerErrorCollection CompilerErrors { get; }
14 |
15 | public ASLCompilerException(ASLMethod method, CompilerErrorCollection errors)
16 | : base(GetMessage(method, errors))
17 | {
18 | Method = method;
19 | CompilerErrors = errors;
20 | }
21 |
22 | private static string GetMessage(ASLMethod method, CompilerErrorCollection errors)
23 | {
24 | if (method == null)
25 | {
26 | throw new ArgumentNullException(nameof(method));
27 | }
28 |
29 | if (errors == null)
30 | {
31 | throw new ArgumentNullException(nameof(errors));
32 | }
33 |
34 | var sb = new StringBuilder($"'{method.Name ?? "(no name)"}' method compilation errors:");
35 | foreach (CompilerError error in errors)
36 | {
37 | error.Line += method.LineOffset;
38 | sb.Append($"\nLine {error.Line}, Col {error.Column}: {(error.IsWarning ? "warning" : "error")} {error.ErrorNumber}: {error.ErrorText}");
39 | }
40 |
41 | return sb.ToString();
42 | }
43 | }
44 |
45 | public class ASLRuntimeException : Exception
46 | {
47 | public ASLRuntimeException(ASLMethod method, Exception inner_exception)
48 | : base(GetMessage(method, inner_exception), inner_exception)
49 | { }
50 |
51 | private static string GetMessage(ASLMethod method, Exception inner_exception)
52 | {
53 | if (method == null)
54 | {
55 | throw new ArgumentNullException(nameof(method));
56 | }
57 |
58 | if (inner_exception == null)
59 | {
60 | throw new ArgumentNullException(nameof(inner_exception));
61 | }
62 |
63 | var stack_trace = new StackTrace(inner_exception, true);
64 | var stack_trace_sb = new StringBuilder();
65 | foreach (StackFrame frame in stack_trace.GetFrames())
66 | {
67 | System.Reflection.MethodBase frame_method = frame.GetMethod();
68 | System.Reflection.Module frame_module = frame_method.Module;
69 |
70 | ASLMethod frame_asl_method = method;
71 | if (method.ScriptMethods != null)
72 | {
73 | frame_asl_method = method.ScriptMethods.FirstOrDefault(m => frame_module == m.Module);
74 | if (frame_asl_method == null)
75 | {
76 | continue;
77 | }
78 | }
79 | else if (frame_module != method.Module)
80 | {
81 | continue;
82 | }
83 |
84 | int frame_line = frame.GetFileLineNumber();
85 | if (frame_line > 0)
86 | {
87 | int line = frame_line + frame_asl_method.LineOffset;
88 | stack_trace_sb.Append($"\n at ASL line {line} in '{frame_asl_method.Name}'");
89 | }
90 | }
91 |
92 | string exception_name = inner_exception.GetType().FullName;
93 | string method_name = method.Name ?? "(no name)";
94 | string exception_message = inner_exception.Message;
95 | return $"Exception thrown: '{exception_name}' in '{method_name}' method:\n{exception_message}\n{stack_trace_sb}";
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLGrammar.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | using Irony.Parsing;
4 |
5 | namespace LiveSplit.ASL;
6 |
7 | [Language("asl", "1.0", "Auto Split Language grammar")]
8 | public class ASLGrammar : Grammar
9 | {
10 | public ASLGrammar()
11 | : base(true)
12 | {
13 | StringLiteral string_lit = TerminalFactory.CreateCSharpString("string");
14 | NumberLiteral number = TerminalFactory.CreateCSharpNumber("number");
15 | number.Options |= NumberOptions.AllowSign;
16 | IdentifierTerminal identifier = TerminalFactory.CreateCSharpIdentifier("identifier");
17 | var code = new CustomTerminal("code", MatchCodeTerminal);
18 |
19 | var single_line_comment = new CommentTerminal("SingleLineComment", "//", "\r", "\n", "\u2085", "\u2028", "\u2029");
20 | var delimited_comment = new CommentTerminal("DelimitedComment", "/*", "*/");
21 | NonGrammarTerminals.Add(single_line_comment);
22 | NonGrammarTerminals.Add(delimited_comment);
23 |
24 | var state = new KeyTerm("state", "state");
25 | var init = new KeyTerm("init", "init");
26 | var exit = new KeyTerm("exit", "exit");
27 | var update = new KeyTerm("update", "update");
28 | var start = new KeyTerm("start", "start");
29 | var split = new KeyTerm("split", "split");
30 | var reset = new KeyTerm("reset", "reset");
31 | var startup = new KeyTerm("startup", "startup");
32 | var shutdown = new KeyTerm("shutdown", "shutdown");
33 | var isLoading = new KeyTerm("isLoading", "isLoading");
34 | var gameTime = new KeyTerm("gameTime", "gameTime");
35 | var onStart = new KeyTerm("onStart", "onStart");
36 | var onSplit = new KeyTerm("onSplit", "onSplit");
37 | var onReset = new KeyTerm("onReset", "onReset");
38 | KeyTerm comma = ToTerm(",", "comma");
39 | KeyTerm semi = ToTerm(";", "semi");
40 |
41 | var root = new NonTerminal("root");
42 | var state_def = new NonTerminal("stateDef");
43 | var version = new NonTerminal("version");
44 | var state_list = new NonTerminal("stateList");
45 | var method_list = new NonTerminal("methodList");
46 | var var_list = new NonTerminal("varList");
47 | var var = new NonTerminal("var");
48 | var module = new NonTerminal("module");
49 | var method = new NonTerminal("method");
50 | var offset_list = new NonTerminal("offsetList");
51 | var offset = new NonTerminal("offset");
52 | var method_type = new NonTerminal("methodType");
53 |
54 | root.Rule = state_list + method_list;
55 | version.Rule = (comma + string_lit) | Empty;
56 | state_def.Rule = state + "(" + string_lit + version + ")" + "{" + var_list + "}";
57 | state_list.Rule = MakeStarRule(state_list, state_def);
58 | method_list.Rule = MakeStarRule(method_list, method);
59 | var_list.Rule = MakeStarRule(var_list, semi, var);
60 | module.Rule = (string_lit + comma) | Empty;
61 | var.Rule = (identifier + identifier + ":" + module + offset_list) | Empty;
62 | method.Rule = (method_type + "{" + code + "}") | Empty;
63 | offset_list.Rule = MakePlusRule(offset_list, comma, offset);
64 | offset.Rule = number;
65 | method_type.Rule = init | exit | update | start | split | isLoading | gameTime | reset | startup | shutdown | onStart | onSplit | onReset;
66 |
67 | Root = root;
68 |
69 | MarkTransient(var_list, method_list, offset, method_type);
70 |
71 | LanguageFlags = LanguageFlags.NewLineBeforeEOF;
72 | }
73 |
74 | private static Token MatchCodeTerminal(Terminal terminal, ParsingContext context, ISourceStream source)
75 | {
76 | string remaining = source.Text[source.Location.Position..];
77 | int stack = 1;
78 | string token = "";
79 |
80 | while (stack > 0)
81 | {
82 | int index = remaining.IndexOf('}') + 1;
83 | string cut = remaining[..index];
84 |
85 | token += cut;
86 | remaining = remaining[index..];
87 | stack += cut.Count(x => x == '{');
88 | stack--;
89 | }
90 |
91 | token = token[..^1];
92 | source.PreviewPosition += token.Length;
93 |
94 | return source.CreateToken(terminal.OutputTerminal);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLMethod.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.CodeDom.Compiler;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Dynamic;
6 | using System.Linq;
7 | using System.Reflection;
8 |
9 | using LiveSplit.Model;
10 |
11 | namespace LiveSplit.ASL;
12 |
13 | public class ASLMethod
14 | {
15 | public ASLScript.Methods ScriptMethods { get; set; }
16 |
17 | public string Name { get; }
18 |
19 | public bool IsEmpty { get; }
20 |
21 | public int LineOffset { get; }
22 |
23 | public Module Module { get; }
24 |
25 | private readonly dynamic _compiled_code;
26 |
27 | public ASLMethod(string code, string name = null, int script_line = 0)
28 | {
29 | if (code == null)
30 | {
31 | throw new ArgumentNullException(nameof(code));
32 | }
33 |
34 | Name = name;
35 | IsEmpty = string.IsNullOrWhiteSpace(code);
36 | code = code.Replace("return;", "return null;"); // hack
37 |
38 | var options = new Dictionary {
39 | { "CompilerVersion", "v4.0" }
40 | };
41 |
42 | using var provider = new Microsoft.CSharp.CSharpCodeProvider(options);
43 | string user_code_start_marker = "// USER_CODE_START";
44 | string source = $@"
45 | using System;
46 | using System.Collections.Generic;
47 | using System.Diagnostics;
48 | using System.Dynamic;
49 | using System.IO;
50 | using System.Linq;
51 | using System.Reflection;
52 | using System.Text;
53 | using System.Threading;
54 | using System.Windows.Forms;
55 | using LiveSplit.ComponentUtil;
56 | using LiveSplit.Model;
57 | using LiveSplit.Options;
58 | public class CompiledScript
59 | {{
60 | public string version;
61 | public double refreshRate;
62 | void print(string s)
63 | {{
64 | Log.Info(s);
65 | }}
66 | public dynamic Execute(LiveSplitState timer, dynamic old, dynamic current, dynamic vars, Process game, dynamic settings)
67 | {{
68 | var memory = game;
69 | var modules = game != null ? game.ModulesWow64Safe() : null;
70 | {user_code_start_marker}
71 | {code}
72 | return null;
73 | }}
74 | }}";
75 |
76 | if (script_line > 0)
77 | {
78 | int user_code_index = source.IndexOf(user_code_start_marker);
79 | int compiled_code_line = source.Take(user_code_index).Count(c => c == '\n') + 2;
80 | LineOffset = script_line - compiled_code_line;
81 | }
82 |
83 | var parameters = new CompilerParameters()
84 | {
85 | GenerateInMemory = true,
86 | CompilerOptions = "/optimize /d:TRACE /debug:pdbonly",
87 | };
88 | parameters.ReferencedAssemblies.Add("System.dll");
89 | parameters.ReferencedAssemblies.Add("System.Core.dll");
90 | parameters.ReferencedAssemblies.Add("System.Data.dll");
91 | parameters.ReferencedAssemblies.Add("System.Data.DataSetExtensions.dll");
92 | parameters.ReferencedAssemblies.Add("System.Drawing.dll");
93 | parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
94 | parameters.ReferencedAssemblies.Add("System.Xml.dll");
95 | parameters.ReferencedAssemblies.Add("System.Xml.Linq.dll");
96 | parameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
97 | parameters.ReferencedAssemblies.Add("LiveSplit.Core.dll");
98 |
99 | CompilerResults res = provider.CompileAssemblyFromSource(parameters, source);
100 | if (res.Errors.HasErrors)
101 | {
102 | throw new ASLCompilerException(this, res.Errors);
103 | }
104 |
105 | Module = res.CompiledAssembly.ManifestModule;
106 | Type type = res.CompiledAssembly.GetType("CompiledScript");
107 | _compiled_code = Activator.CreateInstance(type);
108 | }
109 |
110 | public dynamic Call(LiveSplitState timer, ExpandoObject vars, ref string version, ref double refreshRate,
111 | dynamic settings, ExpandoObject old = null, ExpandoObject current = null, Process game = null)
112 | {
113 | // dynamic args can't be ref or out, this is a workaround
114 | _compiled_code.version = version;
115 | _compiled_code.refreshRate = refreshRate;
116 | dynamic ret;
117 | try
118 | {
119 | ret = _compiled_code.Execute(timer, old, current, vars, game, settings);
120 | }
121 | catch (Exception ex)
122 | {
123 | throw new ASLRuntimeException(this, ex);
124 | }
125 |
126 | version = _compiled_code.version;
127 | refreshRate = _compiled_code.refreshRate;
128 | return ret;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | using Irony.Parsing;
7 |
8 | using LiveSplit.ComponentUtil;
9 |
10 | namespace LiveSplit.ASL;
11 |
12 | public class ASLParser
13 | {
14 | public static ASLScript Parse(string code)
15 | {
16 | var grammar = new ASLGrammar();
17 | var parser = new Parser(grammar);
18 | ParseTree tree = parser.Parse(code);
19 |
20 | if (tree.HasErrors())
21 | {
22 | var error_msg = new StringBuilder("ASL parse error(s):");
23 | foreach (Irony.LogMessage msg in parser.Context.CurrentParseTree.ParserMessages)
24 | {
25 | SourceLocation loc = msg.Location;
26 | error_msg.Append($"\nat Line {loc.Line + 1}, Col {loc.Column + 1}: {msg.Message}");
27 | }
28 |
29 | throw new Exception(error_msg.ToString());
30 | }
31 |
32 | ParseTreeNodeList root_childs = tree.Root.ChildNodes;
33 | ParseTreeNode methods_node = root_childs.First(x => x.Term.Name == "methodList");
34 | ParseTreeNode states_node = root_childs.First(x => x.Term.Name == "stateList");
35 |
36 | var states = new Dictionary>(StringComparer.OrdinalIgnoreCase);
37 |
38 | foreach (ParseTreeNode state_node in states_node.ChildNodes)
39 | {
40 | string process_name = (string)state_node.ChildNodes[2].Token.Value;
41 | string version =
42 | state_node.ChildNodes[3].ChildNodes.Skip(1).Select(x => (string)x.Token.Value).FirstOrDefault() ??
43 | string.Empty;
44 | ParseTreeNodeList value_definition_nodes = state_node.ChildNodes[6].ChildNodes;
45 |
46 | var state = new ASLState();
47 |
48 | foreach (ParseTreeNode value_definition_node in value_definition_nodes.Where(x => x.ChildNodes.Count > 0))
49 | {
50 | ParseTreeNodeList child_nodes = value_definition_node.ChildNodes;
51 | string type = (string)child_nodes[0].Token.Value;
52 | string identifier = (string)child_nodes[1].Token.Value;
53 | string module =
54 | child_nodes[3].ChildNodes.Take(1).Select(x => (string)x.Token.Value).FirstOrDefault() ??
55 | string.Empty;
56 | int module_base = child_nodes[4].ChildNodes.Select(x => (int)x.Token.Value).First();
57 | int[] offsets = child_nodes[4].ChildNodes.Skip(1).Select(x => (int)x.Token.Value).ToArray();
58 | var value_definition = new ASLValueDefinition()
59 | {
60 | Identifier = identifier,
61 | Type = type,
62 | Pointer = new DeepPointer(module, module_base, offsets)
63 | };
64 | state.ValueDefinitions.Add(value_definition);
65 | }
66 |
67 | state.GameVersion = version;
68 | if (!states.ContainsKey(process_name))
69 | {
70 | states.Add(process_name, []);
71 | }
72 |
73 | states[process_name].Add(state);
74 | }
75 |
76 | var methods = new ASLScript.Methods();
77 |
78 | foreach (ParseTreeNode method in methods_node.ChildNodes[0].ChildNodes)
79 | {
80 | string body = (string)method.ChildNodes[2].Token.Value;
81 | string method_name = (string)method.ChildNodes[0].Token.Value;
82 | int line = method.ChildNodes[2].Token.Location.Line + 1;
83 | var script = new ASLMethod(body, method_name, line)
84 | {
85 | ScriptMethods = methods
86 | };
87 | switch (method_name)
88 | {
89 | case "init": methods.init = script; break;
90 | case "exit": methods.exit = script; break;
91 | case "update": methods.update = script; break;
92 | case "start": methods.start = script; break;
93 | case "split": methods.split = script; break;
94 | case "isLoading": methods.isLoading = script; break;
95 | case "gameTime": methods.gameTime = script; break;
96 | case "reset": methods.reset = script; break;
97 | case "startup": methods.startup = script; break;
98 | case "shutdown": methods.shutdown = script; break;
99 | case "onStart": methods.onStart = script; break;
100 | case "onSplit": methods.onSplit = script; break;
101 | case "onReset": methods.onReset = script; break;
102 | }
103 | }
104 |
105 | return new ASLScript(methods, states);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLScript.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Dynamic;
6 | using System.Linq;
7 |
8 | using LiveSplit.Model;
9 | using LiveSplit.Options;
10 |
11 | namespace LiveSplit.ASL;
12 |
13 | public class ASLScript
14 | {
15 | public class Methods : IEnumerable
16 | {
17 | private static readonly ASLMethod no_op = new("");
18 |
19 | public ASLMethod startup = no_op;
20 | public ASLMethod shutdown = no_op;
21 | public ASLMethod init = no_op;
22 | public ASLMethod exit = no_op;
23 | public ASLMethod update = no_op;
24 | public ASLMethod start = no_op;
25 | public ASLMethod split = no_op;
26 | public ASLMethod reset = no_op;
27 | public ASLMethod isLoading = no_op;
28 | public ASLMethod gameTime = no_op;
29 | public ASLMethod onStart = no_op;
30 | public ASLMethod onSplit = no_op;
31 | public ASLMethod onReset = no_op;
32 |
33 | public ASLMethod[] GetMethods()
34 | {
35 | return
36 | [
37 | startup,
38 | shutdown,
39 | init,
40 | exit,
41 | update,
42 | start,
43 | split,
44 | reset,
45 | isLoading,
46 | gameTime,
47 | onStart,
48 | onSplit,
49 | onReset
50 | ];
51 | }
52 |
53 | public IEnumerator GetEnumerator()
54 | {
55 | return ((IEnumerable)GetMethods()).GetEnumerator();
56 | }
57 |
58 | IEnumerator IEnumerable.GetEnumerator()
59 | {
60 | return GetMethods().GetEnumerator();
61 | }
62 | }
63 |
64 | public event EventHandler RefreshRateChanged;
65 | public event EventHandler GameVersionChanged;
66 |
67 | private string _game_version = string.Empty;
68 | public string GameVersion
69 | {
70 | get => _game_version;
71 | set
72 | {
73 | if (value != _game_version)
74 | {
75 | GameVersionChanged?.Invoke(this, value);
76 | }
77 |
78 | _game_version = value;
79 | }
80 | }
81 |
82 | private double _refresh_rate = 1000 / 15d;
83 | public double RefreshRate // per sec
84 | {
85 | get => _refresh_rate;
86 | set
87 | {
88 | if (Math.Abs(value - _refresh_rate) > 0.01)
89 | {
90 | RefreshRateChanged?.Invoke(this, value);
91 | }
92 |
93 | _refresh_rate = value;
94 | }
95 | }
96 |
97 | // public so other components (ASLVarViewer) can access
98 | public ASLState State { get; private set; }
99 | public ASLState OldState { get; private set; }
100 | public ExpandoObject Vars { get; }
101 |
102 | private readonly bool _uses_game_time;
103 | private bool _init_completed;
104 |
105 | private readonly ASLSettings _settings;
106 |
107 | private Process _game;
108 | private TimerModel _timer;
109 |
110 | private readonly Dictionary> _states;
111 |
112 | private readonly Methods _methods;
113 |
114 | public ASLScript(Methods methods, Dictionary> states)
115 | {
116 | _methods = methods;
117 | _states = states;
118 |
119 | _settings = new ASLSettings();
120 | Vars = new ExpandoObject();
121 |
122 | if (!_methods.start.IsEmpty)
123 | {
124 | _settings.AddBasicSetting("start");
125 | }
126 |
127 | if (!_methods.split.IsEmpty)
128 | {
129 | _settings.AddBasicSetting("split");
130 | }
131 |
132 | if (!_methods.reset.IsEmpty)
133 | {
134 | _settings.AddBasicSetting("reset");
135 | }
136 |
137 | _uses_game_time = !_methods.isLoading.IsEmpty || !_methods.gameTime.IsEmpty;
138 | }
139 |
140 | // Update the script
141 | public void Update(LiveSplitState state)
142 | {
143 | if (_game == null)
144 | {
145 | _timer ??= new TimerModel() { CurrentState = state };
146 |
147 | TryConnect(state);
148 | }
149 | else if (_game.HasExited)
150 | {
151 | DoExit(state);
152 | }
153 | else
154 | {
155 | if (!_init_completed)
156 | {
157 | DoInit(state);
158 | }
159 | else
160 | {
161 | DoUpdate(state);
162 | }
163 | }
164 | }
165 |
166 | // Run startup and return settings defined in ASL script.
167 | public ASLSettings RunStartup(LiveSplitState state)
168 | {
169 | Debug("Running startup");
170 | RunNoProcessMethod(_methods.startup, state, true);
171 |
172 | if (!_methods.onStart.IsEmpty)
173 | {
174 | state.OnStart += RunOnStart;
175 | }
176 |
177 | if (!_methods.onSplit.IsEmpty)
178 | {
179 | state.OnSplit += RunOnSplit;
180 | }
181 |
182 | if (!_methods.onReset.IsEmpty)
183 | {
184 | state.OnReset += RunOnReset;
185 | }
186 |
187 | return _settings;
188 | }
189 |
190 | private void RunOnStart(object sender, EventArgs e)
191 | {
192 | RunMethod(_methods.onStart, (LiveSplitState)sender);
193 | }
194 |
195 | private void RunOnSplit(object sender, EventArgs e)
196 | {
197 | RunMethod(_methods.onSplit, (LiveSplitState)sender);
198 | }
199 |
200 | private void RunOnReset(object sender, TimerPhase e)
201 | {
202 | RunMethod(_methods.onReset, (LiveSplitState)sender);
203 | }
204 |
205 | public void RunShutdown(LiveSplitState state)
206 | {
207 | Debug("Running shutdown");
208 |
209 | if (!_methods.onStart.IsEmpty)
210 | {
211 | state.OnStart -= RunOnStart;
212 | }
213 |
214 | if (!_methods.onSplit.IsEmpty)
215 | {
216 | state.OnSplit -= RunOnSplit;
217 | }
218 |
219 | if (!_methods.onReset.IsEmpty)
220 | {
221 | state.OnReset -= RunOnReset;
222 | }
223 |
224 | RunMethod(_methods.shutdown, state);
225 | }
226 |
227 | private void TryConnect(LiveSplitState state)
228 | {
229 | _game = null;
230 |
231 | var state_process = _states.Keys.Select(proccessName => new
232 | {
233 | // default to the state with no version specified, if it exists
234 | State = _states[proccessName].FirstOrDefault(s => s.GameVersion == "") ?? _states[proccessName].First(),
235 | Process = Process.GetProcessesByName(proccessName).OrderByDescending(x => x.StartTime)
236 | .FirstOrDefault(x => !x.HasExited)
237 | }).FirstOrDefault(x => x.Process != null);
238 |
239 | if (state_process == null)
240 | {
241 | return;
242 | }
243 |
244 | _init_completed = false;
245 | _game = state_process.Process;
246 | State = state_process.State;
247 |
248 | if (State.GameVersion == "")
249 | {
250 | Debug("Connected to game: {0} (using default state descriptor)", _game.ProcessName);
251 | }
252 | else
253 | {
254 | Debug("Connected to game: {0} (state descriptor for version '{1}' chosen as default)",
255 | _game.ProcessName,
256 | State.GameVersion);
257 | }
258 |
259 | DoInit(state);
260 | }
261 |
262 | // This is executed each time after connecting to the game (usually just once,
263 | // unless an error occurs before the method finishes).
264 | private void DoInit(LiveSplitState state)
265 | {
266 | Debug("Initializing");
267 |
268 | State.RefreshValues(_game);
269 | OldState = State;
270 | GameVersion = string.Empty;
271 |
272 | // Fetch version from init-method
273 | string ver = string.Empty;
274 | RunMethod(_methods.init, state, ref ver);
275 |
276 | if (ver != GameVersion)
277 | {
278 | GameVersion = ver;
279 |
280 | ASLState version_state = _states.Where(kv => kv.Key.ToLower() == _game.ProcessName.ToLower())
281 | .Select(kv => kv.Value)
282 | .First() // states
283 | .FirstOrDefault(s => s.GameVersion == ver);
284 |
285 | if (version_state != null)
286 | {
287 | // This state descriptor may already be selected
288 | if (version_state != State)
289 | {
290 | State = version_state;
291 | State.RefreshValues(_game);
292 | OldState = State;
293 | Debug($"Switched to state descriptor for version '{GameVersion}'");
294 | }
295 | }
296 | else
297 | {
298 | Debug($"No state descriptor for version '{GameVersion}' (will keep using default one)");
299 | }
300 | }
301 |
302 | _init_completed = true;
303 | Debug("Init completed, running main methods");
304 | }
305 |
306 | private void DoExit(LiveSplitState state)
307 | {
308 | Debug("Running exit");
309 | _game = null;
310 | RunNoProcessMethod(_methods.exit, state);
311 | }
312 |
313 | // This is executed repeatedly as long as the game is connected and initialized.
314 | private void DoUpdate(LiveSplitState state)
315 | {
316 | OldState = State.RefreshValues(_game);
317 |
318 | if (!(RunMethod(_methods.update, state) ?? true))
319 | {
320 | // If Update explicitly returns false, don't run anything else
321 | return;
322 | }
323 |
324 | if (state.CurrentPhase is TimerPhase.Running or TimerPhase.Paused)
325 | {
326 | if (_uses_game_time && !state.IsGameTimeInitialized)
327 | {
328 | _timer.InitializeGameTime();
329 | }
330 |
331 | dynamic is_paused = RunMethod(_methods.isLoading, state);
332 | if (is_paused != null)
333 | {
334 | state.IsGameTimePaused = is_paused;
335 | }
336 |
337 | dynamic game_time = RunMethod(_methods.gameTime, state);
338 | if (game_time != null)
339 | {
340 | state.SetGameTime(game_time);
341 | }
342 |
343 | if (RunMethod(_methods.reset, state) ?? false)
344 | {
345 | if (_settings.GetBasicSettingValue("reset"))
346 | {
347 | _timer.Reset();
348 | }
349 | }
350 | else if (RunMethod(_methods.split, state) ?? false)
351 | {
352 | if (_settings.GetBasicSettingValue("split"))
353 | {
354 | _timer.Split();
355 | }
356 | }
357 | }
358 |
359 | if (state.CurrentPhase == TimerPhase.NotRunning)
360 | {
361 | if (RunMethod(_methods.start, state) ?? false)
362 | {
363 | if (_settings.GetBasicSettingValue("start"))
364 | {
365 | _timer.Start();
366 | }
367 | }
368 | }
369 | }
370 |
371 | private dynamic RunMethod(ASLMethod method, LiveSplitState state, ref string version)
372 | {
373 | double refresh_rate = RefreshRate;
374 | dynamic result = method.Call(state, Vars, ref version, ref refresh_rate, _settings.Reader,
375 | OldState?.Data, State?.Data, _game);
376 | RefreshRate = refresh_rate;
377 | return result;
378 | }
379 |
380 | private dynamic RunMethod(ASLMethod method, LiveSplitState state)
381 | {
382 | string version = GameVersion;
383 | return RunMethod(method, state, ref version);
384 | }
385 |
386 | // Run method without counting on being connected to the game (startup/shutdown).
387 | private void RunNoProcessMethod(ASLMethod method, LiveSplitState state, bool is_startup = false)
388 | {
389 | double refresh_rate = RefreshRate;
390 | string version = GameVersion;
391 | method.Call(state, Vars, ref version, ref refresh_rate,
392 | is_startup ? _settings.Builder : (object)_settings.Reader);
393 | RefreshRate = refresh_rate;
394 | }
395 |
396 | private void Debug(string output, params object[] args)
397 | {
398 | Log.Info(string.Format("[ASL/{1}] {0}",
399 | string.Format(output, args),
400 | GetHashCode()));
401 | }
402 | }
403 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | using LiveSplit.Options;
5 |
6 | namespace LiveSplit.ASL;
7 |
8 | // Created from the ASL script and shared with the GUI to synchronize setting state.
9 | public class ASLSetting
10 | {
11 | public string Id { get; }
12 | public string Label { get; }
13 | public bool Value { get; set; }
14 | public bool DefaultValue { get; }
15 | public string Parent { get; }
16 | public string ToolTip { get; set; }
17 |
18 | public ASLSetting(string id, bool default_value, string label, string parent)
19 | {
20 | Id = id;
21 | Value = default_value;
22 | DefaultValue = default_value;
23 | Label = label;
24 | Parent = parent;
25 | }
26 |
27 | public override string ToString()
28 | {
29 | return Label;
30 | }
31 | }
32 |
33 | public class ASLSettings
34 | {
35 | // Dict for easy access per key
36 | public Dictionary Settings { get; set; }
37 | // List for preserved insertion order (Dict provides that as well, but not guaranteed)
38 | public List OrderedSettings { get; }
39 |
40 | public Dictionary BasicSettings { get; }
41 |
42 | public ASLSettingsBuilder Builder;
43 | public ASLSettingsReader Reader;
44 |
45 | public ASLSettings()
46 | {
47 | Settings = [];
48 | OrderedSettings = [];
49 | BasicSettings = [];
50 | Builder = new ASLSettingsBuilder(this);
51 | Reader = new ASLSettingsReader(this);
52 | }
53 |
54 | public void AddSetting(string name, bool default_value, string description, string parent)
55 | {
56 | description ??= name;
57 |
58 | if (parent != null && !Settings.ContainsKey(parent))
59 | {
60 | throw new ArgumentException($"Parent for setting '{name}' is not a setting: {parent}");
61 | }
62 |
63 | if (Settings.ContainsKey(name))
64 | {
65 | throw new ArgumentException($"Setting '{name}' was already added");
66 | }
67 |
68 | var setting = new ASLSetting(name, default_value, description, parent);
69 | Settings.Add(name, setting);
70 | OrderedSettings.Add(setting);
71 | }
72 |
73 | public bool GetSettingValue(string name)
74 | {
75 | // Don't cause error if setting doesn't exist, but still inform script
76 | // author since that usually shouldn't happen.
77 | if (Settings.ContainsKey(name))
78 | {
79 | return GetSettingValueRecursive(Settings[name]);
80 | }
81 |
82 | Log.Info("[ASL] Custom Setting Key doesn't exist: " + name);
83 |
84 | return false;
85 | }
86 |
87 | public void AddBasicSetting(string name)
88 | {
89 | BasicSettings.Add(name, new ASLSetting(name, true, "", null));
90 | }
91 |
92 | public bool GetBasicSettingValue(string name)
93 | {
94 | if (BasicSettings.ContainsKey(name))
95 | {
96 | return BasicSettings[name].Value;
97 | }
98 |
99 | return false;
100 | }
101 |
102 | public bool IsBasicSettingPresent(string name)
103 | {
104 | return BasicSettings.ContainsKey(name);
105 | }
106 |
107 | ///
108 | /// Returns true only if this setting and all it's parent settings are true.
109 | ///
110 | private bool GetSettingValueRecursive(ASLSetting setting)
111 | {
112 | if (!setting.Value)
113 | {
114 | return false;
115 | }
116 |
117 | if (setting.Parent == null)
118 | {
119 | return setting.Value;
120 | }
121 |
122 | return GetSettingValueRecursive(Settings[setting.Parent]);
123 | }
124 | }
125 |
126 | ///
127 | /// Interface for adding settings via the ASL Script.
128 | ///
129 | public class ASLSettingsBuilder
130 | {
131 | public string CurrentDefaultParent { get; set; }
132 | private readonly ASLSettings _s;
133 |
134 | public ASLSettingsBuilder(ASLSettings s)
135 | {
136 | _s = s;
137 | }
138 |
139 | public void Add(string id, bool default_value = true, string description = null, string parent = null)
140 | {
141 | parent ??= CurrentDefaultParent;
142 |
143 | _s.AddSetting(id, default_value, description, parent);
144 | }
145 |
146 | public void SetToolTip(string id, string text)
147 | {
148 | if (!_s.Settings.ContainsKey(id))
149 | {
150 | throw new ArgumentException($"Can't set tooltip, '{id}' is not a setting");
151 | }
152 |
153 | _s.Settings[id].ToolTip = text;
154 | }
155 | }
156 |
157 | ///
158 | /// Interface for reading settings via the ASL Script.
159 | ///
160 | public class ASLSettingsReader
161 | {
162 | private readonly ASLSettings _s;
163 |
164 | public ASLSettingsReader(ASLSettings s)
165 | {
166 | _s = s;
167 | }
168 |
169 | public dynamic this[string id] => _s.GetSettingValue(id);
170 |
171 | public bool ContainsKey(string key)
172 | {
173 | return _s.Settings.ContainsKey(key);
174 | }
175 |
176 | public bool StartEnabled => _s.GetBasicSettingValue("start");
177 |
178 | public bool ResetEnabled => _s.GetBasicSettingValue("reset");
179 |
180 | public bool SplitEnabled => _s.GetBasicSettingValue("split");
181 | }
182 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ASL/ASLState.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Dynamic;
5 |
6 | using LiveSplit.ComponentUtil;
7 |
8 | namespace LiveSplit.ASL;
9 |
10 | public class ASLValueDefinition
11 | {
12 | public string Type { get; set; }
13 | public string Identifier { get; set; }
14 | public DeepPointer Pointer { get; set; }
15 | }
16 |
17 | public class ASLState : ICloneable
18 | {
19 | public ExpandoObject Data { get; set; }
20 | public List ValueDefinitions { get; set; }
21 | public string GameVersion { get; set; }
22 |
23 | public ASLState()
24 | {
25 | Data = new ExpandoObject();
26 | ValueDefinitions = [];
27 | GameVersion = string.Empty;
28 | }
29 |
30 | public ASLState RefreshValues(Process p)
31 | {
32 | var clone = (ASLState)Clone();
33 | var dict = (IDictionary)Data;
34 |
35 | foreach (ASLValueDefinition value_definition in ValueDefinitions)
36 | {
37 | dynamic value = GetValue(p, value_definition.Type, value_definition.Pointer);
38 |
39 | if (dict.ContainsKey(value_definition.Identifier))
40 | {
41 | dict[value_definition.Identifier] = value;
42 | }
43 | else
44 | {
45 | dict.Add(value_definition.Identifier, value);
46 | }
47 | }
48 |
49 | return clone;
50 | }
51 |
52 | private static dynamic GetValue(Process p, string type, DeepPointer pointer)
53 | {
54 | switch (type)
55 | {
56 | case "int":
57 | return pointer.Deref(p);
58 | case "uint":
59 | return pointer.Deref(p);
60 | case "long":
61 | return pointer.Deref(p);
62 | case "ulong":
63 | return pointer.Deref(p);
64 | case "float":
65 | return pointer.Deref(p);
66 | case "double":
67 | return pointer.Deref(p);
68 | case "byte":
69 | return pointer.Deref(p);
70 | case "sbyte":
71 | return pointer.Deref(p);
72 | case "short":
73 | return pointer.Deref(p);
74 | case "ushort":
75 | return pointer.Deref(p);
76 | case "bool":
77 | return pointer.Deref(p);
78 | default:
79 | if (type.StartsWith("string"))
80 | {
81 | int length = int.Parse(type["string".Length..]);
82 | return pointer.DerefString(p, length);
83 | }
84 | else if (type.StartsWith("byte"))
85 | {
86 | int length = int.Parse(type["byte".Length..]);
87 | return pointer.DerefBytes(p, length);
88 | }
89 |
90 | break;
91 | }
92 |
93 | throw new ArgumentException($"The provided type, '{type}', is not supported");
94 | }
95 |
96 | public object Clone()
97 | {
98 | var clone = new ExpandoObject();
99 | foreach (KeyValuePair pair in Data)
100 | {
101 | ((IDictionary)clone).Add(pair);
102 | }
103 |
104 | return new ASLState() { Data = clone, ValueDefinitions = new List(ValueDefinitions) };
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/Component.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using System.Windows.Forms;
5 | using System.Xml;
6 |
7 | using LiveSplit.ASL;
8 | using LiveSplit.Model;
9 | using LiveSplit.Options;
10 |
11 | namespace LiveSplit.UI.Components;
12 |
13 | public class ASLComponent : LogicComponent
14 | {
15 | public override string ComponentName => "Scriptable Auto Splitter";
16 |
17 | // public so other components (ASLVarViewer) can access
18 | public ASLScript Script { get; private set; }
19 |
20 | public event EventHandler ScriptChanged;
21 |
22 | private bool _do_reload;
23 | private string _old_script_path;
24 |
25 | private readonly Timer _update_timer;
26 | private readonly FileSystemWatcher _fs_watcher;
27 |
28 | private readonly ComponentSettings _settings;
29 |
30 | private readonly LiveSplitState _state;
31 |
32 | public ASLComponent(LiveSplitState state)
33 | {
34 | _state = state;
35 |
36 | _settings = new ComponentSettings();
37 |
38 | _fs_watcher = new FileSystemWatcher();
39 |
40 | async void handler(object sender, T args)
41 | {
42 | await Task.Delay(200);
43 | _do_reload = true;
44 | }
45 |
46 | _fs_watcher.Changed += handler;
47 | _fs_watcher.Renamed += handler;
48 |
49 | // -try- to run a little faster than 60hz
50 | // note: Timer isn't very reliable and quite often takes ~30ms
51 | // we need to switch to threading
52 | _update_timer = new Timer() { Interval = 15 };
53 | _update_timer.Tick += (sender, args) => UpdateScript();
54 | _update_timer.Enabled = true;
55 | }
56 |
57 | public ASLComponent(LiveSplitState state, string script_path)
58 | : this(state)
59 | {
60 | _settings = new ComponentSettings(script_path);
61 | }
62 |
63 | public override void Dispose()
64 | {
65 | ScriptCleanup();
66 |
67 | try
68 | {
69 | ScriptChanged?.Invoke(this, EventArgs.Empty);
70 | }
71 | catch (Exception ex)
72 | {
73 | Log.Error(ex);
74 | }
75 |
76 | _fs_watcher?.Dispose();
77 | _update_timer?.Dispose();
78 | }
79 |
80 | public override Control GetSettingsControl(LayoutMode mode)
81 | {
82 | return _settings;
83 | }
84 |
85 | public override XmlNode GetSettings(XmlDocument document)
86 | {
87 | return _settings.GetSettings(document);
88 | }
89 |
90 | public override void SetSettings(XmlNode settings)
91 | {
92 | _settings.SetSettings(settings);
93 | }
94 |
95 | public override void Update(IInvalidator invalidator, LiveSplitState state, float width, float height,
96 | LayoutMode mode)
97 | { }
98 |
99 | private void UpdateScript()
100 | {
101 | // Disable timer, to wait for execution of this iteration to
102 | // finish. This can be useful if blocking operations like
103 | // showing a message window are used.
104 | _update_timer.Enabled = false;
105 |
106 | // this is ugly, fix eventually!
107 | if (_settings.ScriptPath != _old_script_path || _do_reload)
108 | {
109 | try
110 | {
111 | _do_reload = false;
112 | _old_script_path = _settings.ScriptPath;
113 |
114 | ScriptCleanup();
115 |
116 | if (string.IsNullOrEmpty(_settings.ScriptPath))
117 | {
118 | // Only disable file watcher if script path changed to empty
119 | // (otherwise detecting file changes may still be wanted)
120 | _fs_watcher.EnableRaisingEvents = false;
121 | }
122 | else
123 | {
124 | LoadScript();
125 | }
126 |
127 | ScriptChanged?.Invoke(this, EventArgs.Empty);
128 | }
129 | catch (Exception ex)
130 | {
131 | Log.Error(ex);
132 | }
133 | }
134 |
135 | if (Script != null)
136 | {
137 | try
138 | {
139 | Script.Update(_state);
140 | }
141 | catch (Exception ex)
142 | {
143 | Log.Error(ex);
144 | }
145 | }
146 |
147 | _update_timer.Enabled = true;
148 | }
149 |
150 | private void LoadScript()
151 | {
152 | Log.Info("[ASL] Loading new script: " + _settings.ScriptPath);
153 |
154 | _fs_watcher.Path = Path.GetDirectoryName(_settings.ScriptPath);
155 | _fs_watcher.Filter = Path.GetFileName(_settings.ScriptPath);
156 | _fs_watcher.EnableRaisingEvents = true;
157 |
158 | // New script
159 | Script = ASLParser.Parse(File.ReadAllText(_settings.ScriptPath));
160 |
161 | Script.RefreshRateChanged += (sender, rate) => _update_timer.Interval = (int)Math.Round(1000 / rate);
162 | _update_timer.Interval = (int)Math.Round(1000 / Script.RefreshRate);
163 |
164 | Script.GameVersionChanged += (sender, version) => _settings.SetGameVersion(version);
165 | _settings.SetGameVersion(null);
166 |
167 | // Give custom ASL settings to GUI, which populates the list and
168 | // stores the ASLSetting objects which are shared between the GUI
169 | // and ASLScript
170 | try
171 | {
172 | ASLSettings settings = Script.RunStartup(_state);
173 | _settings.SetASLSettings(settings);
174 | }
175 | catch (Exception ex)
176 | {
177 | // Script already created, but startup failed, so clean up again
178 | Log.Error(ex);
179 | ScriptCleanup();
180 | }
181 | }
182 |
183 | private void ScriptCleanup()
184 | {
185 | if (Script == null)
186 | {
187 | return;
188 | }
189 |
190 | try
191 | {
192 | Script.RunShutdown(_state);
193 | }
194 | catch (Exception ex)
195 | {
196 | Log.Error(ex);
197 | }
198 | finally
199 | {
200 | _settings.SetGameVersion(null);
201 | _settings.ResetASLSettings();
202 |
203 | // Script should no longer be used, even in case of error
204 | // (which the ASL shutdown method may contain)
205 | Script = null;
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ComponentSettings.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace LiveSplit.UI.Components
2 | {
3 | partial class ComponentSettings
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Component Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.components = new System.ComponentModel.Container();
32 | this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
33 | this.labelScriptPath = new System.Windows.Forms.Label();
34 | this.btnSelectFile = new System.Windows.Forms.Button();
35 | this.txtScriptPath = new System.Windows.Forms.TextBox();
36 | this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
37 | this.btnCheckAll = new System.Windows.Forms.Button();
38 | this.btnUncheckAll = new System.Windows.Forms.Button();
39 | this.btnResetToDefault = new System.Windows.Forms.Button();
40 | this.flowLayoutPanel2 = new System.Windows.Forms.FlowLayoutPanel();
41 | this.checkboxStart = new System.Windows.Forms.CheckBox();
42 | this.checkboxSplit = new System.Windows.Forms.CheckBox();
43 | this.checkboxReset = new System.Windows.Forms.CheckBox();
44 | this.lblGameVersion = new System.Windows.Forms.Label();
45 | this.labelOptions = new System.Windows.Forms.Label();
46 | this.labelCustomSettings = new System.Windows.Forms.Label();
47 | this.treeContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
48 | this.cmiExpandTree = new System.Windows.Forms.ToolStripMenuItem();
49 | this.cmiCollapseTree = new System.Windows.Forms.ToolStripMenuItem();
50 | this.cmiCollapseTreeToSelection = new System.Windows.Forms.ToolStripMenuItem();
51 | this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
52 | this.cmiExpandBranch = new System.Windows.Forms.ToolStripMenuItem();
53 | this.cmiCollapseBranch = new System.Windows.Forms.ToolStripMenuItem();
54 | this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
55 | this.cmiCheckBranch = new System.Windows.Forms.ToolStripMenuItem();
56 | this.cmiUncheckBranch = new System.Windows.Forms.ToolStripMenuItem();
57 | this.cmiResetBranchToDefault = new System.Windows.Forms.ToolStripMenuItem();
58 | this.treeContextMenu2 = new System.Windows.Forms.ContextMenuStrip(this.components);
59 | this.cmiExpandTree2 = new System.Windows.Forms.ToolStripMenuItem();
60 | this.cmiCollapseTree2 = new System.Windows.Forms.ToolStripMenuItem();
61 | this.cmiCollapseTreeToSelection2 = new System.Windows.Forms.ToolStripMenuItem();
62 | this.cmiResetSettingToDefault = new System.Windows.Forms.ToolStripMenuItem();
63 | this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
64 | this.resetSettingToDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
65 | this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
66 | this.treeCustomSettings = new LiveSplit.UI.Components.NewTreeView();
67 | this.tableLayoutPanel1.SuspendLayout();
68 | this.flowLayoutPanel1.SuspendLayout();
69 | this.flowLayoutPanel2.SuspendLayout();
70 | this.treeContextMenu.SuspendLayout();
71 | this.treeContextMenu2.SuspendLayout();
72 | this.SuspendLayout();
73 | //
74 | // tableLayoutPanel1
75 | //
76 | this.tableLayoutPanel1.AutoSize = true;
77 | this.tableLayoutPanel1.ColumnCount = 3;
78 | this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 76F));
79 | this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
80 | this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
81 | this.tableLayoutPanel1.Controls.Add(this.labelScriptPath, 0, 0);
82 | this.tableLayoutPanel1.Controls.Add(this.btnSelectFile, 2, 0);
83 | this.tableLayoutPanel1.Controls.Add(this.txtScriptPath, 1, 0);
84 | this.tableLayoutPanel1.Controls.Add(this.flowLayoutPanel1, 1, 3);
85 | this.tableLayoutPanel1.Controls.Add(this.flowLayoutPanel2, 1, 1);
86 | this.tableLayoutPanel1.Controls.Add(this.labelOptions, 0, 1);
87 | this.tableLayoutPanel1.Controls.Add(this.labelCustomSettings, 0, 2);
88 | this.tableLayoutPanel1.Controls.Add(this.treeCustomSettings, 1, 2);
89 | this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
90 | this.tableLayoutPanel1.Location = new System.Drawing.Point(7, 7);
91 | this.tableLayoutPanel1.Name = "tableLayoutPanel1";
92 | this.tableLayoutPanel1.RowCount = 4;
93 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 29F));
94 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
95 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
96 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
97 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
98 | this.tableLayoutPanel1.Size = new System.Drawing.Size(462, 498);
99 | this.tableLayoutPanel1.TabIndex = 0;
100 | //
101 | // labelScriptPath
102 | //
103 | this.labelScriptPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
104 | this.labelScriptPath.AutoSize = true;
105 | this.labelScriptPath.Location = new System.Drawing.Point(3, 8);
106 | this.labelScriptPath.Name = "labelScriptPath";
107 | this.labelScriptPath.Size = new System.Drawing.Size(70, 13);
108 | this.labelScriptPath.TabIndex = 3;
109 | this.labelScriptPath.Text = "Script Path:";
110 | //
111 | // btnSelectFile
112 | //
113 | this.btnSelectFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
114 | this.btnSelectFile.Location = new System.Drawing.Point(385, 3);
115 | this.btnSelectFile.Name = "btnSelectFile";
116 | this.btnSelectFile.Size = new System.Drawing.Size(74, 23);
117 | this.btnSelectFile.TabIndex = 1;
118 | this.btnSelectFile.Text = "Browse...";
119 | this.btnSelectFile.UseVisualStyleBackColor = true;
120 | this.btnSelectFile.Click += new System.EventHandler(this.btnSelectFile_Click);
121 | //
122 | // txtScriptPath
123 | //
124 | this.txtScriptPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
125 | this.txtScriptPath.Location = new System.Drawing.Point(79, 4);
126 | this.txtScriptPath.Name = "txtScriptPath";
127 | this.txtScriptPath.Size = new System.Drawing.Size(300, 20);
128 | this.txtScriptPath.TabIndex = 0;
129 | //
130 | // flowLayoutPanel1
131 | //
132 | this.flowLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
133 | this.flowLayoutPanel1.AutoSize = true;
134 | this.tableLayoutPanel1.SetColumnSpan(this.flowLayoutPanel1, 2);
135 | this.flowLayoutPanel1.Controls.Add(this.btnCheckAll);
136 | this.flowLayoutPanel1.Controls.Add(this.btnUncheckAll);
137 | this.flowLayoutPanel1.Controls.Add(this.btnResetToDefault);
138 | this.flowLayoutPanel1.Location = new System.Drawing.Point(205, 457);
139 | this.flowLayoutPanel1.Name = "flowLayoutPanel1";
140 | this.flowLayoutPanel1.Size = new System.Drawing.Size(254, 29);
141 | this.flowLayoutPanel1.TabIndex = 8;
142 | //
143 | // btnCheckAll
144 | //
145 | this.btnCheckAll.Location = new System.Drawing.Point(3, 3);
146 | this.btnCheckAll.Name = "btnCheckAll";
147 | this.btnCheckAll.Size = new System.Drawing.Size(62, 23);
148 | this.btnCheckAll.TabIndex = 5;
149 | this.btnCheckAll.Text = "Check All";
150 | this.btnCheckAll.UseVisualStyleBackColor = true;
151 | this.btnCheckAll.Click += new System.EventHandler(this.btnCheckAll_Click);
152 | //
153 | // btnUncheckAll
154 | //
155 | this.btnUncheckAll.AutoSize = true;
156 | this.btnUncheckAll.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
157 | this.btnUncheckAll.Location = new System.Drawing.Point(71, 3);
158 | this.btnUncheckAll.Name = "btnUncheckAll";
159 | this.btnUncheckAll.Size = new System.Drawing.Size(75, 23);
160 | this.btnUncheckAll.TabIndex = 6;
161 | this.btnUncheckAll.Text = "Uncheck All";
162 | this.btnUncheckAll.UseVisualStyleBackColor = true;
163 | this.btnUncheckAll.Click += new System.EventHandler(this.btnUncheckAll_Click);
164 | //
165 | // btnResetToDefault
166 | //
167 | this.btnResetToDefault.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
168 | this.btnResetToDefault.Location = new System.Drawing.Point(152, 3);
169 | this.btnResetToDefault.Name = "btnResetToDefault";
170 | this.btnResetToDefault.Size = new System.Drawing.Size(99, 23);
171 | this.btnResetToDefault.TabIndex = 7;
172 | this.btnResetToDefault.Text = "Reset to Default";
173 | this.btnResetToDefault.UseVisualStyleBackColor = true;
174 | this.btnResetToDefault.Click += new System.EventHandler(this.btnResetToDefault_Click);
175 | //
176 | // flowLayoutPanel2
177 | //
178 | this.flowLayoutPanel2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
179 | this.flowLayoutPanel2.AutoSize = true;
180 | this.tableLayoutPanel1.SetColumnSpan(this.flowLayoutPanel2, 2);
181 | this.flowLayoutPanel2.Controls.Add(this.checkboxStart);
182 | this.flowLayoutPanel2.Controls.Add(this.checkboxSplit);
183 | this.flowLayoutPanel2.Controls.Add(this.checkboxReset);
184 | this.flowLayoutPanel2.Controls.Add(this.lblGameVersion);
185 | this.flowLayoutPanel2.Location = new System.Drawing.Point(79, 32);
186 | this.flowLayoutPanel2.Name = "flowLayoutPanel2";
187 | this.flowLayoutPanel2.Size = new System.Drawing.Size(380, 23);
188 | this.flowLayoutPanel2.TabIndex = 12;
189 | //
190 | // checkboxStart
191 | //
192 | this.checkboxStart.Enabled = false;
193 | this.checkboxStart.Location = new System.Drawing.Point(3, 3);
194 | this.checkboxStart.Name = "checkboxStart";
195 | this.checkboxStart.Size = new System.Drawing.Size(48, 17);
196 | this.checkboxStart.TabIndex = 11;
197 | this.checkboxStart.Text = "Start";
198 | this.checkboxStart.UseVisualStyleBackColor = true;
199 | this.checkboxStart.CheckedChanged += new System.EventHandler(this.methodCheckbox_CheckedChanged);
200 | //
201 | // checkboxSplit
202 | //
203 | this.checkboxSplit.Enabled = false;
204 | this.checkboxSplit.Location = new System.Drawing.Point(57, 3);
205 | this.checkboxSplit.Name = "checkboxSplit";
206 | this.checkboxSplit.Size = new System.Drawing.Size(46, 17);
207 | this.checkboxSplit.TabIndex = 0;
208 | this.checkboxSplit.Text = "Split";
209 | this.checkboxSplit.UseVisualStyleBackColor = true;
210 | this.checkboxSplit.CheckedChanged += new System.EventHandler(this.methodCheckbox_CheckedChanged);
211 | //
212 | // checkboxReset
213 | //
214 | this.checkboxReset.Enabled = false;
215 | this.checkboxReset.Location = new System.Drawing.Point(109, 3);
216 | this.checkboxReset.Name = "checkboxReset";
217 | this.checkboxReset.Size = new System.Drawing.Size(54, 17);
218 | this.checkboxReset.TabIndex = 0;
219 | this.checkboxReset.Text = "Reset";
220 | this.checkboxReset.UseVisualStyleBackColor = true;
221 | this.checkboxReset.CheckedChanged += new System.EventHandler(this.methodCheckbox_CheckedChanged);
222 | //
223 | // lblGameVersion
224 | //
225 | this.lblGameVersion.Anchor = System.Windows.Forms.AnchorStyles.Right;
226 | this.lblGameVersion.AutoEllipsis = true;
227 | this.lblGameVersion.Location = new System.Drawing.Point(169, 5);
228 | this.lblGameVersion.Name = "lblGameVersion";
229 | this.lblGameVersion.Size = new System.Drawing.Size(208, 13);
230 | this.lblGameVersion.TabIndex = 10;
231 | this.lblGameVersion.Text = "Game Version: 1.0";
232 | this.lblGameVersion.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
233 | //
234 | // labelOptions
235 | //
236 | this.labelOptions.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
237 | this.labelOptions.AutoSize = true;
238 | this.labelOptions.Location = new System.Drawing.Point(3, 37);
239 | this.labelOptions.Name = "labelOptions";
240 | this.labelOptions.Size = new System.Drawing.Size(70, 13);
241 | this.labelOptions.TabIndex = 9;
242 | this.labelOptions.Text = "Options:";
243 | //
244 | // labelCustomSettings
245 | //
246 | this.labelCustomSettings.AutoSize = true;
247 | this.labelCustomSettings.Location = new System.Drawing.Point(3, 63);
248 | this.labelCustomSettings.Margin = new System.Windows.Forms.Padding(3, 5, 3, 0);
249 | this.labelCustomSettings.Name = "labelCustomSettings";
250 | this.labelCustomSettings.Size = new System.Drawing.Size(59, 13);
251 | this.labelCustomSettings.TabIndex = 13;
252 | this.labelCustomSettings.Text = "Advanced:";
253 | //
254 | // treeContextMenu
255 | //
256 | this.treeContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
257 | this.cmiExpandTree,
258 | this.cmiCollapseTree,
259 | this.cmiCollapseTreeToSelection,
260 | this.toolStripSeparator1,
261 | this.cmiExpandBranch,
262 | this.cmiCollapseBranch,
263 | this.toolStripSeparator2,
264 | this.cmiCheckBranch,
265 | this.cmiUncheckBranch,
266 | this.cmiResetBranchToDefault,
267 | this.toolStripSeparator3,
268 | this.cmiResetSettingToDefault});
269 | this.treeContextMenu.Name = "treeContextMenu";
270 | this.treeContextMenu.Size = new System.Drawing.Size(199, 220);
271 | //
272 | // cmiExpandTree
273 | //
274 | this.cmiExpandTree.Name = "cmiExpandTree";
275 | this.cmiExpandTree.Size = new System.Drawing.Size(198, 22);
276 | this.cmiExpandTree.Text = "Expand Tree";
277 | this.cmiExpandTree.Click += new System.EventHandler(this.cmiExpandTree_Click);
278 | //
279 | // cmiCollapseTree
280 | //
281 | this.cmiCollapseTree.Name = "cmiCollapseTree";
282 | this.cmiCollapseTree.Size = new System.Drawing.Size(198, 22);
283 | this.cmiCollapseTree.Text = "Collapse Tree";
284 | this.cmiCollapseTree.Click += new System.EventHandler(this.cmiCollapseTree_Click);
285 | //
286 | // cmiCollapseTreeToSelection
287 | //
288 | this.cmiCollapseTreeToSelection.Name = "cmiCollapseTreeToSelection";
289 | this.cmiCollapseTreeToSelection.Size = new System.Drawing.Size(198, 22);
290 | this.cmiCollapseTreeToSelection.Text = "Collapse Tree to Selection";
291 | this.cmiCollapseTreeToSelection.Click += new System.EventHandler(this.cmiCollapseTreeToSelection_Click);
292 | //
293 | // toolStripSeparator1
294 | //
295 | this.toolStripSeparator1.Name = "toolStripSeparator1";
296 | this.toolStripSeparator1.Size = new System.Drawing.Size(195, 6);
297 | //
298 | // cmiExpandBranch
299 | //
300 | this.cmiExpandBranch.Name = "cmiExpandBranch";
301 | this.cmiExpandBranch.Size = new System.Drawing.Size(198, 22);
302 | this.cmiExpandBranch.Text = "Expand Branch";
303 | this.cmiExpandBranch.Click += new System.EventHandler(this.cmiExpandBranch_Click);
304 | //
305 | // cmiCollapseBranch
306 | //
307 | this.cmiCollapseBranch.Name = "cmiCollapseBranch";
308 | this.cmiCollapseBranch.Size = new System.Drawing.Size(198, 22);
309 | this.cmiCollapseBranch.Text = "Collapse Branch";
310 | this.cmiCollapseBranch.Click += new System.EventHandler(this.cmiCollapseBranch_Click);
311 | //
312 | // toolStripSeparator2
313 | //
314 | this.toolStripSeparator2.Name = "toolStripSeparator2";
315 | this.toolStripSeparator2.Size = new System.Drawing.Size(195, 6);
316 | //
317 | // cmiCheckBranch
318 | //
319 | this.cmiCheckBranch.Name = "cmiCheckBranch";
320 | this.cmiCheckBranch.Size = new System.Drawing.Size(198, 22);
321 | this.cmiCheckBranch.Text = "Check Branch";
322 | this.cmiCheckBranch.Click += new System.EventHandler(this.cmiCheckBranch_Click);
323 | //
324 | // cmiUncheckBranch
325 | //
326 | this.cmiUncheckBranch.Name = "cmiUncheckBranch";
327 | this.cmiUncheckBranch.Size = new System.Drawing.Size(198, 22);
328 | this.cmiUncheckBranch.Text = "Uncheck Branch";
329 | this.cmiUncheckBranch.Click += new System.EventHandler(this.cmiUncheckBranch_Click);
330 | //
331 | // cmiResetBranchToDefault
332 | //
333 | this.cmiResetBranchToDefault.Name = "cmiResetBranchToDefault";
334 | this.cmiResetBranchToDefault.Size = new System.Drawing.Size(198, 22);
335 | this.cmiResetBranchToDefault.Text = "Reset Branch to Default";
336 | this.cmiResetBranchToDefault.Click += new System.EventHandler(this.cmiResetBranchToDefault_Click);
337 | //
338 | // treeContextMenu2
339 | //
340 | this.treeContextMenu2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
341 | this.cmiExpandTree2,
342 | this.cmiCollapseTree2,
343 | this.cmiCollapseTreeToSelection2,
344 | this.toolStripSeparator4,
345 | this.resetSettingToDefaultToolStripMenuItem});
346 | this.treeContextMenu2.Name = "treeContextMenu";
347 | this.treeContextMenu2.Size = new System.Drawing.Size(199, 98);
348 | //
349 | // cmiExpandTree2
350 | //
351 | this.cmiExpandTree2.Name = "cmiExpandTree2";
352 | this.cmiExpandTree2.Size = new System.Drawing.Size(198, 22);
353 | this.cmiExpandTree2.Text = "Expand Tree";
354 | this.cmiExpandTree2.Click += new System.EventHandler(this.cmiExpandTree_Click);
355 | //
356 | // cmiCollapseTree2
357 | //
358 | this.cmiCollapseTree2.Name = "cmiCollapseTree2";
359 | this.cmiCollapseTree2.Size = new System.Drawing.Size(198, 22);
360 | this.cmiCollapseTree2.Text = "Collapse Tree";
361 | this.cmiCollapseTree2.Click += new System.EventHandler(this.cmiCollapseTree_Click);
362 | //
363 | // cmiCollapseTreeToSelection2
364 | //
365 | this.cmiCollapseTreeToSelection2.Name = "cmiCollapseTreeToSelection2";
366 | this.cmiCollapseTreeToSelection2.Size = new System.Drawing.Size(198, 22);
367 | this.cmiCollapseTreeToSelection2.Text = "Collapse Tree to Selection";
368 | this.cmiCollapseTreeToSelection2.Click += new System.EventHandler(this.cmiCollapseTreeToSelection_Click);
369 | //
370 | // cmiResetSettingToDefault
371 | //
372 | this.cmiResetSettingToDefault.Name = "cmiResetSettingToDefault";
373 | this.cmiResetSettingToDefault.Size = new System.Drawing.Size(198, 22);
374 | this.cmiResetSettingToDefault.Text = "Reset Setting to Default";
375 | this.cmiResetSettingToDefault.Click += new System.EventHandler(this.cmiResetSettingToDefault_Click);
376 | //
377 | // toolStripSeparator3
378 | //
379 | this.toolStripSeparator3.Name = "toolStripSeparator3";
380 | this.toolStripSeparator3.Size = new System.Drawing.Size(195, 6);
381 | //
382 | // resetSettingToDefaultToolStripMenuItem
383 | //
384 | this.resetSettingToDefaultToolStripMenuItem.Name = "resetSettingToDefaultToolStripMenuItem";
385 | this.resetSettingToDefaultToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
386 | this.resetSettingToDefaultToolStripMenuItem.Text = "Reset Setting to Default";
387 | this.resetSettingToDefaultToolStripMenuItem.Click += new System.EventHandler(this.cmiResetSettingToDefault_Click);
388 | //
389 | // toolStripSeparator4
390 | //
391 | this.toolStripSeparator4.Name = "toolStripSeparator4";
392 | this.toolStripSeparator4.Size = new System.Drawing.Size(195, 6);
393 | //
394 | // treeCustomSettings
395 | //
396 | this.treeCustomSettings.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
397 | | System.Windows.Forms.AnchorStyles.Left)
398 | | System.Windows.Forms.AnchorStyles.Right)));
399 | this.treeCustomSettings.CheckBoxes = true;
400 | this.tableLayoutPanel1.SetColumnSpan(this.treeCustomSettings, 2);
401 | this.treeCustomSettings.Location = new System.Drawing.Point(79, 61);
402 | this.treeCustomSettings.Name = "treeCustomSettings";
403 | this.treeCustomSettings.ShowNodeToolTips = true;
404 | this.treeCustomSettings.Size = new System.Drawing.Size(380, 390);
405 | this.treeCustomSettings.TabIndex = 14;
406 | this.treeCustomSettings.BeforeCheck += new System.Windows.Forms.TreeViewCancelEventHandler(this.settingsTree_BeforeCheck);
407 | this.treeCustomSettings.AfterCheck += new System.Windows.Forms.TreeViewEventHandler(this.settingsTree_AfterCheck);
408 | this.treeCustomSettings.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.settingsTree_NodeMouseClick);
409 | //
410 | // ComponentSettings
411 | //
412 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
413 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
414 | this.Controls.Add(this.tableLayoutPanel1);
415 | this.Name = "ComponentSettings";
416 | this.Padding = new System.Windows.Forms.Padding(7);
417 | this.Size = new System.Drawing.Size(476, 512);
418 | this.tableLayoutPanel1.ResumeLayout(false);
419 | this.tableLayoutPanel1.PerformLayout();
420 | this.flowLayoutPanel1.ResumeLayout(false);
421 | this.flowLayoutPanel1.PerformLayout();
422 | this.flowLayoutPanel2.ResumeLayout(false);
423 | this.treeContextMenu.ResumeLayout(false);
424 | this.treeContextMenu2.ResumeLayout(false);
425 | this.ResumeLayout(false);
426 | this.PerformLayout();
427 |
428 | }
429 |
430 | #endregion
431 |
432 | private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
433 | private System.Windows.Forms.Label labelScriptPath;
434 | private System.Windows.Forms.Button btnSelectFile;
435 | public System.Windows.Forms.TextBox txtScriptPath;
436 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
437 | private System.Windows.Forms.Button btnCheckAll;
438 | private System.Windows.Forms.Button btnUncheckAll;
439 | private System.Windows.Forms.Button btnResetToDefault;
440 | private System.Windows.Forms.Label labelOptions;
441 | private System.Windows.Forms.Label lblGameVersion;
442 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel2;
443 | private System.Windows.Forms.CheckBox checkboxStart;
444 | private System.Windows.Forms.CheckBox checkboxReset;
445 | private System.Windows.Forms.CheckBox checkboxSplit;
446 | private System.Windows.Forms.Label labelCustomSettings;
447 | private NewTreeView treeCustomSettings;
448 | private System.Windows.Forms.ContextMenuStrip treeContextMenu;
449 | private System.Windows.Forms.ToolStripMenuItem cmiCheckBranch;
450 | private System.Windows.Forms.ToolStripMenuItem cmiUncheckBranch;
451 | private System.Windows.Forms.ToolStripMenuItem cmiResetBranchToDefault;
452 | private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
453 | private System.Windows.Forms.ToolStripMenuItem cmiExpandBranch;
454 | private System.Windows.Forms.ToolStripMenuItem cmiCollapseBranch;
455 | private System.Windows.Forms.ToolStripMenuItem cmiCollapseTreeToSelection;
456 | private System.Windows.Forms.ContextMenuStrip treeContextMenu2;
457 | private System.Windows.Forms.ToolStripMenuItem cmiCollapseTreeToSelection2;
458 | private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
459 | private System.Windows.Forms.ToolStripMenuItem cmiExpandTree;
460 | private System.Windows.Forms.ToolStripMenuItem cmiCollapseTree;
461 | private System.Windows.Forms.ToolStripMenuItem cmiExpandTree2;
462 | private System.Windows.Forms.ToolStripMenuItem cmiCollapseTree2;
463 | private System.Windows.Forms.ToolStripMenuItem cmiResetSettingToDefault;
464 | private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
465 | private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
466 | private System.Windows.Forms.ToolStripMenuItem resetSettingToDefaultToolStripMenuItem;
467 | }
468 | }
469 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ComponentSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using System.IO;
5 | using System.Windows.Forms;
6 | using System.Xml;
7 |
8 | using LiveSplit.ASL;
9 |
10 | namespace LiveSplit.UI.Components;
11 |
12 | public partial class ComponentSettings : UserControl
13 | {
14 | public string ScriptPath { get; set; }
15 |
16 | // if true, next path loaded from settings will be ignored
17 | private bool _ignore_next_path_setting;
18 |
19 | private readonly Dictionary _basic_settings;
20 |
21 | // Save the state of settings independant of actual ASLSetting objects
22 | // or the actual GUI components (checkboxes). This is used to restore
23 | // the state when the script is first loaded (because settings are
24 | // loaded before the script) or reloaded.
25 | //
26 | // State is synchronized with the ASLSettings when a script is
27 | // successfully loaded, as well as when the checkboxes/tree check
28 | // state is changed by the user or program. It is also updated
29 | // when loaded from XML.
30 | //
31 | // State is stored from the current script, or the last loaded script
32 | // if no script is currently loaded.
33 |
34 | // Start/Reset/Split checkboxes
35 | private readonly Dictionary _basic_settings_state;
36 |
37 | // Custom settings
38 | private Dictionary _custom_settings_state;
39 |
40 | public ComponentSettings()
41 | {
42 | InitializeComponent();
43 |
44 | ScriptPath = string.Empty;
45 |
46 | txtScriptPath.DataBindings.Add("Text", this, "ScriptPath", false,
47 | DataSourceUpdateMode.OnPropertyChanged);
48 |
49 | SetGameVersion(null);
50 | UpdateCustomSettingsVisibility();
51 |
52 | _basic_settings = new Dictionary
53 | {
54 | // Capitalized names for saving it in XML.
55 | ["Start"] = checkboxStart,
56 | ["Reset"] = checkboxReset,
57 | ["Split"] = checkboxSplit
58 | };
59 |
60 | _basic_settings_state = [];
61 | _custom_settings_state = [];
62 | }
63 |
64 | public ComponentSettings(string scriptPath)
65 | : this()
66 | {
67 | ScriptPath = scriptPath;
68 | _ignore_next_path_setting = true;
69 | }
70 |
71 | public XmlNode GetSettings(XmlDocument document)
72 | {
73 | XmlElement settings_node = document.CreateElement("Settings");
74 |
75 | settings_node.AppendChild(SettingsHelper.ToElement(document, "Version", "1.5"));
76 | settings_node.AppendChild(SettingsHelper.ToElement(document, "ScriptPath", ScriptPath));
77 | AppendBasicSettingsToXml(document, settings_node);
78 | AppendCustomSettingsToXml(document, settings_node);
79 |
80 | return settings_node;
81 | }
82 |
83 | // Loads the settings of this component from Xml. This might happen more than once
84 | // (e.g. when the settings dialog is cancelled, to restore previous settings).
85 | public void SetSettings(XmlNode settings)
86 | {
87 | var element = (XmlElement)settings;
88 | if (!element.IsEmpty)
89 | {
90 | if (!_ignore_next_path_setting)
91 | {
92 | ScriptPath = SettingsHelper.ParseString(element["ScriptPath"], string.Empty);
93 | }
94 |
95 | _ignore_next_path_setting = false;
96 | ParseBasicSettingsFromXml(element);
97 | ParseCustomSettingsFromXml(element);
98 | }
99 | }
100 |
101 | public void SetGameVersion(string version)
102 | {
103 | lblGameVersion.Text = string.IsNullOrEmpty(version) ? "" : "Game Version: " + version;
104 | }
105 |
106 | ///
107 | /// Populates the component with the settings defined in the ASL script.
108 | ///
109 | public void SetASLSettings(ASLSettings settings)
110 | {
111 | InitASLSettings(settings, true);
112 | }
113 |
114 | ///
115 | /// Empties the GUI of all settings (but still keeps settings state
116 | /// for the next script load).
117 | ///
118 | public void ResetASLSettings()
119 | {
120 | InitASLSettings(new ASLSettings(), false);
121 | }
122 |
123 | private void InitASLSettings(ASLSettings settings, bool script_loaded)
124 | {
125 | if (string.IsNullOrWhiteSpace(ScriptPath))
126 | {
127 | _basic_settings_state.Clear();
128 | _custom_settings_state.Clear();
129 | }
130 |
131 | treeCustomSettings.BeginUpdate();
132 | treeCustomSettings.Nodes.Clear();
133 |
134 | var values = new Dictionary();
135 |
136 | // Store temporary for easier lookup of parent nodes
137 | var flat = new Dictionary();
138 |
139 | foreach (ASLSetting setting in settings.OrderedSettings)
140 | {
141 | bool value = setting.Value;
142 | if (_custom_settings_state.ContainsKey(setting.Id))
143 | {
144 | value = _custom_settings_state[setting.Id];
145 | }
146 |
147 | var node = new TreeNode(setting.Label)
148 | {
149 | Tag = setting,
150 | Checked = value,
151 | ContextMenuStrip = treeContextMenu2,
152 | ToolTipText = setting.ToolTip
153 | };
154 | setting.Value = value;
155 |
156 | if (setting.Parent == null)
157 | {
158 | treeCustomSettings.Nodes.Add(node);
159 | }
160 | else if (flat.ContainsKey(setting.Parent))
161 | {
162 | flat[setting.Parent].Nodes.Add(node);
163 | flat[setting.Parent].ContextMenuStrip = treeContextMenu;
164 | }
165 |
166 | flat.Add(setting.Id, node);
167 | values.Add(setting.Id, value);
168 | }
169 |
170 | // Gray out deactivated nodes after all have been added
171 | foreach (KeyValuePair item in flat)
172 | {
173 | if (!item.Value.Checked)
174 | {
175 | UpdateGrayedOut(item.Value);
176 | }
177 | }
178 |
179 | // Only if a script was actually loaded, update current state with current ASL settings
180 | // (which may be empty if the successfully loaded script has no settings, but shouldn't
181 | // be empty because the script failed to load, which can happen frequently when working
182 | // on ASL scripts)
183 | if (script_loaded)
184 | {
185 | _custom_settings_state = values;
186 | }
187 |
188 | treeCustomSettings.ExpandAll();
189 | treeCustomSettings.EndUpdate();
190 |
191 | // Scroll up to the top
192 | if (treeCustomSettings.Nodes.Count > 0)
193 | {
194 | treeCustomSettings.Nodes[0].EnsureVisible();
195 | }
196 |
197 | UpdateCustomSettingsVisibility();
198 | InitBasicSettings(settings);
199 | }
200 |
201 | private void AppendBasicSettingsToXml(XmlDocument document, XmlNode settings_node)
202 | {
203 | foreach (KeyValuePair item in _basic_settings)
204 | {
205 | if (_basic_settings_state.ContainsKey(item.Key.ToLower()))
206 | {
207 | bool value = _basic_settings_state[item.Key.ToLower()];
208 | settings_node.AppendChild(SettingsHelper.ToElement(document, item.Key, value));
209 | }
210 | }
211 | }
212 |
213 | private void AppendCustomSettingsToXml(XmlDocument document, XmlNode parent)
214 | {
215 | XmlElement asl_parent = document.CreateElement("CustomSettings");
216 |
217 | foreach (KeyValuePair setting in _custom_settings_state)
218 | {
219 | XmlElement element = SettingsHelper.ToElement(document, "Setting", setting.Value);
220 | XmlAttribute id = SettingsHelper.ToAttribute(document, "id", setting.Key);
221 | // In case there are other setting types in the future
222 | XmlAttribute type = SettingsHelper.ToAttribute(document, "type", "bool");
223 |
224 | element.Attributes.Append(id);
225 | element.Attributes.Append(type);
226 | asl_parent.AppendChild(element);
227 | }
228 |
229 | parent.AppendChild(asl_parent);
230 | }
231 |
232 | private void ParseBasicSettingsFromXml(XmlElement element)
233 | {
234 | foreach (KeyValuePair item in _basic_settings)
235 | {
236 | if (element[item.Key] != null)
237 | {
238 | bool value = bool.Parse(element[item.Key].InnerText);
239 |
240 | // If component is not enabled, don't check setting
241 | if (item.Value.Enabled)
242 | {
243 | item.Value.Checked = value;
244 | }
245 |
246 | _basic_settings_state[item.Key.ToLower()] = value;
247 | }
248 | }
249 | }
250 |
251 | ///
252 | /// Parses custom settings, stores them and updates the checked state of already added tree nodes.
253 | ///
254 | ///
255 | private void ParseCustomSettingsFromXml(XmlElement data)
256 | {
257 | XmlElement custom_settings_node = data["CustomSettings"];
258 |
259 | if (custom_settings_node != null && custom_settings_node.HasChildNodes)
260 | {
261 | foreach (XmlElement element in custom_settings_node.ChildNodes)
262 | {
263 | if (element.Name != "Setting")
264 | {
265 | continue;
266 | }
267 |
268 | string id = element.Attributes["id"].Value;
269 | string type = element.Attributes["type"].Value;
270 |
271 | if (id != null && type == "bool")
272 | {
273 | bool value = SettingsHelper.ParseBool(element);
274 | _custom_settings_state[id] = value;
275 | }
276 | }
277 | }
278 |
279 | // Update tree with loaded state (in case the tree is already populated)
280 | UpdateNodesCheckedState(_custom_settings_state);
281 | }
282 |
283 | private void InitBasicSettings(ASLSettings settings)
284 | {
285 | foreach (KeyValuePair item in _basic_settings)
286 | {
287 | string name = item.Key.ToLower();
288 | CheckBox checkbox = item.Value;
289 |
290 | if (settings.IsBasicSettingPresent(name))
291 | {
292 | ASLSetting setting = settings.BasicSettings[name];
293 | checkbox.Enabled = true;
294 | checkbox.Tag = setting;
295 | bool value = setting.Value;
296 |
297 | if (_basic_settings_state.ContainsKey(name))
298 | {
299 | value = _basic_settings_state[name];
300 | }
301 |
302 | checkbox.Checked = value;
303 | setting.Value = value;
304 | }
305 | else
306 | {
307 | checkbox.Tag = null;
308 | checkbox.Enabled = false;
309 | checkbox.Checked = false;
310 | }
311 | }
312 | }
313 |
314 | private void UpdateCustomSettingsVisibility()
315 | {
316 | bool show = treeCustomSettings.GetNodeCount(false) > 0;
317 | treeCustomSettings.Visible = show;
318 | btnResetToDefault.Visible = show;
319 | btnCheckAll.Visible = show;
320 | btnUncheckAll.Visible = show;
321 | labelCustomSettings.Visible = show;
322 | }
323 |
324 | ///
325 | /// Generic update on all given nodes and their childnodes, ignoring childnodes for
326 | /// nodes where the Func returns false.
327 | ///
328 | ///
329 | private void UpdateNodesInTree(Func func, TreeNodeCollection nodes)
330 | {
331 | foreach (TreeNode node in nodes)
332 | {
333 | bool include_child_nodes = func(node);
334 | if (include_child_nodes)
335 | {
336 | UpdateNodesInTree(func, node.Nodes);
337 | }
338 | }
339 | }
340 |
341 | ///
342 | /// Update the checked state of all given nodes and their childnodes based on the return
343 | /// value of the given Func.
344 | ///
345 | /// If nodes is null, all nodes of the custom settings tree are affected.
346 | ///
347 | private void UpdateNodesCheckedState(Func func, TreeNodeCollection nodes = null)
348 | {
349 | nodes ??= treeCustomSettings.Nodes;
350 |
351 | UpdateNodesInTree(node =>
352 | {
353 | var setting = (ASLSetting)node.Tag;
354 | bool check = func(setting);
355 |
356 | if (node.Checked != check)
357 | {
358 | node.Checked = check;
359 | }
360 |
361 | return true;
362 | }, nodes);
363 | }
364 |
365 | ///
366 | /// Update the checked state of all given nodes and their childnodes
367 | /// based on a dictionary of setting values.
368 | ///
369 | ///
370 | private void UpdateNodesCheckedState(Dictionary setting_values, TreeNodeCollection nodes = null)
371 | {
372 | if (setting_values == null)
373 | {
374 | return;
375 | }
376 |
377 | UpdateNodesCheckedState(setting =>
378 | {
379 | string id = setting.Id;
380 |
381 | if (setting_values.ContainsKey(id))
382 | {
383 | return setting_values[id];
384 | }
385 |
386 | return setting.Value;
387 | }, nodes);
388 | }
389 |
390 | private void UpdateNodeCheckedState(Func func, TreeNode node)
391 | {
392 | var setting = (ASLSetting)node.Tag;
393 | bool check = func(setting);
394 |
395 | if (node.Checked != check)
396 | {
397 | node.Checked = check;
398 | }
399 | }
400 |
401 | ///
402 | /// If the given node is unchecked, grays out all childnodes.
403 | ///
404 | private void UpdateGrayedOut(TreeNode node)
405 | {
406 | // Only change color of childnodes if this node isn't already grayed out
407 | if (node.ForeColor != SystemColors.GrayText)
408 | {
409 | UpdateNodesInTree(n =>
410 | {
411 | n.ForeColor = node.Checked ? SystemColors.WindowText : SystemColors.GrayText;
412 | return n.Checked || !node.Checked;
413 | }, node.Nodes);
414 | }
415 | }
416 |
417 | // Events
418 |
419 | private void btnSelectFile_Click(object sender, EventArgs e)
420 | {
421 | var dialog = new OpenFileDialog()
422 | {
423 | Filter = "Auto Split Script (*.asl)|*.asl|All Files (*.*)|*.*"
424 | };
425 | if (File.Exists(ScriptPath))
426 | {
427 | dialog.InitialDirectory = Path.GetDirectoryName(ScriptPath);
428 | dialog.FileName = Path.GetFileName(ScriptPath);
429 | }
430 |
431 | if (dialog.ShowDialog() == DialogResult.OK)
432 | {
433 | ScriptPath = txtScriptPath.Text = dialog.FileName;
434 | }
435 | }
436 |
437 | // Basic Setting checked/unchecked
438 | //
439 | // This detects both changes made by the user and by the program, so this should
440 | // change the state in _basic_settings_state fine as well.
441 | private void methodCheckbox_CheckedChanged(object sender, EventArgs e)
442 | {
443 | var checkbox = (CheckBox)sender;
444 | var setting = (ASLSetting)checkbox.Tag;
445 |
446 | if (setting != null)
447 | {
448 | setting.Value = checkbox.Checked;
449 | _basic_settings_state[setting.Id] = setting.Value;
450 | }
451 | }
452 |
453 | // Custom Setting checked/unchecked (only after initially building the tree)
454 | private void settingsTree_AfterCheck(object sender, TreeViewEventArgs e)
455 | {
456 | // Update value in the ASLSetting object, which also changes it in the ASL script
457 | var setting = (ASLSetting)e.Node.Tag;
458 | setting.Value = e.Node.Checked;
459 | _custom_settings_state[setting.Id] = setting.Value;
460 |
461 | UpdateGrayedOut(e.Node);
462 | }
463 |
464 | private void settingsTree_BeforeCheck(object sender, TreeViewCancelEventArgs e)
465 | {
466 | // Confirm that the user initiated the selection
467 | if (e.Action != TreeViewAction.Unknown)
468 | {
469 | e.Cancel = e.Node.ForeColor == SystemColors.GrayText;
470 | }
471 | }
472 |
473 | // Custom Settings Button Events
474 |
475 | private void btnCheckAll_Click(object sender, EventArgs e)
476 | {
477 | UpdateNodesCheckedState(s => true);
478 | }
479 |
480 | private void btnUncheckAll_Click(object sender, EventArgs e)
481 | {
482 | UpdateNodesCheckedState(s => false);
483 | }
484 |
485 | private void btnResetToDefault_Click(object sender, EventArgs e)
486 | {
487 | UpdateNodesCheckedState(s => s.DefaultValue);
488 | }
489 |
490 | // Custom Settings Context Menu Events
491 |
492 | private void settingsTree_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
493 | {
494 | // Select clicked node (not only with left-click) for use with context menu
495 | treeCustomSettings.SelectedNode = e.Node;
496 | }
497 |
498 | private void cmiCheckBranch_Click(object sender, EventArgs e)
499 | {
500 | UpdateNodesCheckedState(s => true, treeCustomSettings.SelectedNode.Nodes);
501 | UpdateNodeCheckedState(s => true, treeCustomSettings.SelectedNode);
502 | }
503 |
504 | private void cmiUncheckBranch_Click(object sender, EventArgs e)
505 | {
506 | UpdateNodesCheckedState(s => false, treeCustomSettings.SelectedNode.Nodes);
507 | UpdateNodeCheckedState(s => false, treeCustomSettings.SelectedNode);
508 | }
509 |
510 | private void cmiResetBranchToDefault_Click(object sender, EventArgs e)
511 | {
512 | UpdateNodesCheckedState(s => s.DefaultValue, treeCustomSettings.SelectedNode.Nodes);
513 | UpdateNodeCheckedState(s => s.DefaultValue, treeCustomSettings.SelectedNode);
514 | }
515 |
516 | private void cmiExpandBranch_Click(object sender, EventArgs e)
517 | {
518 | treeCustomSettings.SelectedNode.ExpandAll();
519 | treeCustomSettings.SelectedNode.EnsureVisible();
520 | }
521 |
522 | private void cmiCollapseBranch_Click(object sender, EventArgs e)
523 | {
524 | treeCustomSettings.SelectedNode.Collapse();
525 | treeCustomSettings.SelectedNode.EnsureVisible();
526 | }
527 |
528 | private void cmiCollapseTreeToSelection_Click(object sender, EventArgs e)
529 | {
530 | TreeNode selected = treeCustomSettings.SelectedNode;
531 | treeCustomSettings.CollapseAll();
532 | treeCustomSettings.SelectedNode = selected;
533 | selected.EnsureVisible();
534 | }
535 |
536 | private void cmiExpandTree_Click(object sender, EventArgs e)
537 | {
538 | treeCustomSettings.ExpandAll();
539 | treeCustomSettings.SelectedNode.EnsureVisible();
540 | }
541 |
542 | private void cmiCollapseTree_Click(object sender, EventArgs e)
543 | {
544 | treeCustomSettings.CollapseAll();
545 | }
546 |
547 | private void cmiResetSettingToDefault_Click(object sender, EventArgs e)
548 | {
549 | UpdateNodeCheckedState(s => s.DefaultValue, treeCustomSettings.SelectedNode);
550 | }
551 | }
552 |
553 | ///
554 | /// TreeView with fixed double-clicking on checkboxes.
555 | ///
556 | ///
557 | /// See also:
558 | /// http://stackoverflow.com/questions/17356976/treeview-with-checkboxes-not-processing-clicks-correctly
559 | /// http://stackoverflow.com/questions/14647216/c-sharp-treeview-ignore-double-click-only-at-checkbox
560 | internal class NewTreeView : TreeView
561 | {
562 | protected override void WndProc(ref Message m)
563 | {
564 | if (m.Msg == 0x203) // identified double click
565 | {
566 | Point local_pos = PointToClient(Cursor.Position);
567 | TreeViewHitTestInfo hit_test_info = HitTest(local_pos);
568 |
569 | if (hit_test_info.Location == TreeViewHitTestLocations.StateImage)
570 | {
571 | m.Msg = 0x201; // if checkbox was clicked, turn into single click
572 | }
573 |
574 | base.WndProc(ref m);
575 | }
576 | else
577 | {
578 | base.WndProc(ref m);
579 | }
580 | }
581 | }
582 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/ComponentSettings.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | 17, 17
122 |
123 |
124 | 162, 17
125 |
126 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/Factory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using LiveSplit.Model;
4 | using LiveSplit.UI.Components;
5 |
6 | [assembly: ComponentFactory(typeof(Factory))]
7 |
8 | namespace LiveSplit.UI.Components;
9 |
10 | public class Factory : IComponentFactory
11 | {
12 | public string ComponentName => "Scriptable Auto Splitter";
13 | public string Description => "Allows scripts written in the ASL language to define the splitting behaviour.";
14 | public ComponentCategory Category => ComponentCategory.Control;
15 | public Version Version => Version.Parse("1.8.30");
16 |
17 | public string UpdateName => ComponentName;
18 | public string UpdateURL => "http://livesplit.org/update/";
19 | public string XMLURL => "http://livesplit.org/update/Components/update.LiveSplit.ScriptableAutoSplit.xml";
20 |
21 | public IComponent Create(LiveSplitState state)
22 | {
23 | return new ASLComponent(state);
24 | }
25 |
26 | public IComponent Create(LiveSplitState state, string script)
27 | {
28 | return new ASLComponent(state, script);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/LiveSplit.ScriptableAutoSplit/LiveSplit.ScriptableAutoSplit.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | LiveSplit
5 | true
6 | net4.8.1
7 |
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------