minecraft-datapack-language 15.4.27__py3-none-any.whl → 15.4.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. minecraft_datapack_language/__init__.py +17 -2
  2. minecraft_datapack_language/_version.py +2 -2
  3. minecraft_datapack_language/ast_nodes.py +87 -59
  4. minecraft_datapack_language/mdl_compiler.py +470 -0
  5. minecraft_datapack_language/mdl_errors.py +14 -0
  6. minecraft_datapack_language/mdl_lexer.py +624 -0
  7. minecraft_datapack_language/mdl_parser.py +573 -0
  8. minecraft_datapack_language-15.4.29.dist-info/METADATA +266 -0
  9. minecraft_datapack_language-15.4.29.dist-info/RECORD +16 -0
  10. minecraft_datapack_language/cli.py +0 -159
  11. minecraft_datapack_language/cli_build.py +0 -1292
  12. minecraft_datapack_language/cli_check.py +0 -155
  13. minecraft_datapack_language/cli_colors.py +0 -264
  14. minecraft_datapack_language/cli_help.py +0 -508
  15. minecraft_datapack_language/cli_new.py +0 -300
  16. minecraft_datapack_language/cli_utils.py +0 -276
  17. minecraft_datapack_language/expression_processor.py +0 -352
  18. minecraft_datapack_language/linter.py +0 -409
  19. minecraft_datapack_language/mdl_lexer_js.py +0 -754
  20. minecraft_datapack_language/mdl_parser_js.py +0 -1049
  21. minecraft_datapack_language/pack.py +0 -758
  22. minecraft_datapack_language-15.4.27.dist-info/METADATA +0 -1274
  23. minecraft_datapack_language-15.4.27.dist-info/RECORD +0 -25
  24. {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/WHEEL +0 -0
  25. {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/entry_points.txt +0 -0
  26. {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/licenses/LICENSE +0 -0
  27. {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/top_level.txt +0 -0
@@ -1,1292 +0,0 @@
1
- """
2
- CLI Build Functions - Core build functionality for MDL CLI
3
- """
4
-
5
- import os
6
- import shutil
7
- import zipfile
8
- from pathlib import Path
9
- from typing import Dict, List, Optional, Any
10
-
11
- from .mdl_lexer_js import lex_mdl_js
12
- from .mdl_parser_js import parse_mdl_js
13
- from .expression_processor import ExpressionProcessor
14
- from .dir_map import get_dir_map
15
- from .pack import Pack, Namespace, Function, Tag, Recipe, Advancement, LootTable, Predicate, ItemModifier, Structure
16
- from .mdl_errors import MDLErrorCollector, create_error, MDLBuildError, MDLFileError, MDLCompilationError, MDLSyntaxError, MDLParserError, MDLLexerError
17
- from .cli_utils import ensure_dir, write_json, _process_variable_substitutions, _convert_condition_to_minecraft_syntax, _find_mdl_files, _validate_selector, _resolve_selector, _extract_base_variable_name, _slugify
18
- from .cli_colors import (
19
- print_success, print_warning, print_error, print_info,
20
- print_section, print_separator, color
21
- )
22
-
23
-
24
- def _extract_scope_selector(var_name: str) -> tuple[str, str]:
25
- """Extract scope selector from variable name like 'player_score<@s>' -> ('player_score', '@s')"""
26
- if '<' in var_name and var_name.endswith('>'):
27
- parts = var_name.split('<', 1)
28
- if len(parts) == 2:
29
- base_name = parts[0]
30
- scope_selector = parts[1][:-1] # Remove trailing >
31
- return base_name, scope_selector
32
- return var_name, "@s" # Default to @s if no scope specified
33
-
34
-
35
- class BuildContext:
36
- """Context for build operations to prevent race conditions."""
37
-
38
- def __init__(self):
39
- self.conditional_functions = []
40
- self.variable_scopes = {}
41
- self.namespace_functions = {}
42
- self.expression_processor = ExpressionProcessor()
43
-
44
-
45
- def _merge_mdl_files(files: List[Path], verbose: bool = False, error_collector: MDLErrorCollector = None) -> Optional[Dict[str, Any]]:
46
- """Merge multiple MDL files into a single AST."""
47
- if not files:
48
- return None
49
-
50
- # Parse the first file to get the base AST
51
- try:
52
- if verbose:
53
- print(f"DEBUG: Parsing first file: {files[0]}")
54
-
55
- with open(files[0], 'r', encoding='utf-8') as f:
56
- content = f.read()
57
-
58
- ast = parse_mdl_js(content, str(files[0]))
59
-
60
- if verbose:
61
- print(f"DEBUG: Successfully parsed {files[0]}")
62
-
63
- except MDLLexerError as e:
64
- if error_collector:
65
- error_collector.add_error(e)
66
- else:
67
- raise
68
- return None
69
- except MDLParserError as e:
70
- if error_collector:
71
- error_collector.add_error(e)
72
- else:
73
- raise
74
- return None
75
- except MDLSyntaxError as e:
76
- if error_collector:
77
- error_collector.add_error(e)
78
- else:
79
- raise
80
- return None
81
- except Exception as e:
82
- if error_collector:
83
- error_collector.add_error(create_error(
84
- MDLCompilationError,
85
- f"Failed to parse {files[0]}: {str(e)}",
86
- file_path=str(files[0]),
87
- suggestion="Check the file syntax and ensure it's a valid MDL file."
88
- ))
89
- else:
90
- raise
91
- return None
92
-
93
- # Merge additional files
94
- for file_path in files[1:]:
95
- try:
96
- if verbose:
97
- print(f"DEBUG: Parsing additional file: {file_path}")
98
-
99
- with open(file_path, 'r', encoding='utf-8') as f:
100
- content = f.read()
101
-
102
- additional_ast = parse_mdl_js(content, str(file_path))
103
-
104
- if verbose:
105
- print(f"DEBUG: Successfully parsed {file_path}")
106
-
107
- # Merge functions
108
- if 'functions' in additional_ast:
109
- if 'functions' not in ast:
110
- ast['functions'] = []
111
- ast['functions'].extend(additional_ast['functions'])
112
-
113
- # Merge variables
114
- if 'variables' in additional_ast:
115
- if 'variables' not in ast:
116
- ast['variables'] = []
117
- ast['variables'].extend(additional_ast['variables'])
118
-
119
- # Merge namespaces (append to existing list)
120
- if 'namespaces' in additional_ast:
121
- if 'namespaces' not in ast:
122
- ast['namespaces'] = []
123
- ast['namespaces'].extend(additional_ast['namespaces'])
124
-
125
- # Merge registry declarations
126
- for registry_type in ['recipes', 'loot_tables', 'advancements', 'predicates', 'item_modifiers', 'structures']:
127
- if registry_type in additional_ast:
128
- if registry_type not in ast:
129
- ast[registry_type] = []
130
- ast[registry_type].extend(additional_ast[registry_type])
131
-
132
- # Merge pack metadata (use the first one found)
133
- if 'pack' in additional_ast and 'pack' not in ast:
134
- ast['pack'] = additional_ast['pack']
135
-
136
- except MDLLexerError as e:
137
- if error_collector:
138
- error_collector.add_error(e)
139
- else:
140
- raise
141
- return None
142
- except MDLParserError as e:
143
- if error_collector:
144
- error_collector.add_error(e)
145
- else:
146
- raise
147
- return None
148
- except MDLSyntaxError as e:
149
- if error_collector:
150
- error_collector.add_error(e)
151
- else:
152
- raise
153
- return None
154
- except Exception as e:
155
- if error_collector:
156
- error_collector.add_error(create_error(
157
- MDLCompilationError,
158
- f"Failed to parse {file_path}: {str(e)}",
159
- file_path=str(file_path),
160
- suggestion="Check the file syntax and ensure it's a valid MDL file."
161
- ))
162
- else:
163
- raise
164
- return None
165
-
166
- return ast
167
-
168
-
169
- def _generate_scoreboard_objectives(ast: Dict[str, Any], output_dir: Path) -> List[str]:
170
- """Generate scoreboard objectives for all variables."""
171
- scoreboard_commands = []
172
-
173
- # Collect all variable names in order of appearance
174
- variables = []
175
- seen_variables = set()
176
-
177
- # From variable declarations (preserve order)
178
- if 'variables' in ast:
179
- for var_decl in ast['variables']:
180
- if 'name' in var_decl and var_decl['name'] not in seen_variables:
181
- variables.append(var_decl['name'])
182
- seen_variables.add(var_decl['name'])
183
-
184
- # From functions (scan for variable usage)
185
- if 'functions' in ast:
186
- for func in ast['functions']:
187
- if 'body' in func:
188
- for statement in func['body']:
189
- # Look for variable assignments and usage
190
- if statement['type'] == 'variable_assignment':
191
- if statement['name'] not in seen_variables:
192
- variables.append(statement['name'])
193
- seen_variables.add(statement['name'])
194
- elif statement['type'] == 'command':
195
- # Scan command for variable substitutions
196
- command = statement['command']
197
- import re
198
- var_matches = re.findall(r'\$([^$]+)\$', command)
199
- for var_name in var_matches:
200
- # Extract base name from scoped variables
201
- base_name = _extract_base_variable_name(var_name)
202
- if base_name not in seen_variables:
203
- variables.append(base_name)
204
- seen_variables.add(base_name)
205
-
206
- # Create scoreboard objectives in the order they were found
207
- for var_name in variables:
208
- # Extract base variable name from scoped variables
209
- base_var_name, _ = _extract_scope_selector(var_name)
210
- scoreboard_commands.append(f"scoreboard objectives add {base_var_name} dummy")
211
-
212
- # Add temporary variables for complex expressions (up to 10 temp variables)
213
- for i in range(10):
214
- scoreboard_commands.append(f"scoreboard objectives add temp_{i} dummy")
215
-
216
- return scoreboard_commands
217
-
218
-
219
- def _generate_load_function(scoreboard_commands: List[str], output_dir: Path, namespace: str, ast: Dict[str, Any]) -> None:
220
- """Generate the load function with scoreboard setup."""
221
- load_content = []
222
-
223
- # Add armor stand setup for server-side operations
224
- load_content.append("execute unless entity @e[type=armor_stand,tag=mdl_server,limit=1] run summon armor_stand ~ 320 ~ {Tags:[\"mdl_server\"],Invisible:1b,Marker:1b,NoGravity:1b,Invulnerable:1b}")
225
-
226
- # Add scoreboard objectives
227
- load_content.extend(scoreboard_commands)
228
-
229
- # Add any custom load commands from the AST
230
- if 'load' in ast:
231
- for command in ast['load']:
232
- load_content.append(command)
233
-
234
- # Write the load function
235
- load_dir = output_dir / "data" / namespace / "function"
236
- ensure_dir(str(load_dir))
237
-
238
- with open(load_dir / "load.mcfunction", 'w', encoding='utf-8') as f:
239
- f.write('\n'.join(load_content))
240
-
241
-
242
- def _process_say_command_with_variables(content: str, selector: str, variable_scopes: Dict[str, str] = None) -> str:
243
- """Process say command content with variable substitution, converting to tellraw with score components."""
244
- import re
245
-
246
- print(f"DEBUG: _process_say_command_with_variables called with content: {repr(content)}, selector: {selector}")
247
- print(f"DEBUG: Variable scopes available: {variable_scopes}")
248
-
249
- # Clean up the content - remove quotes if present
250
- content = content.strip()
251
- if content.startswith('"') and content.endswith('"'):
252
- content = content[1:-1] # Remove surrounding quotes
253
-
254
- # Look for traditional $variable$ syntax
255
- var_pattern = r'\$([^$]+)\$'
256
- matches = re.findall(var_pattern, content)
257
-
258
- if not matches:
259
- # No variables, return simple tellraw
260
- return f'tellraw @a [{{"text":"{content}"}}]'
261
-
262
- # Use re.sub to replace variables with placeholders, then split by placeholders
263
- # This avoids the issue with re.split including captured groups
264
- placeholder_content = content
265
- var_placeholders = []
266
-
267
- for i, match in enumerate(matches):
268
- placeholder = f"__VAR_{i}__"
269
- var_placeholders.append((placeholder, match))
270
- placeholder_content = placeholder_content.replace(f"${match}$", placeholder, 1)
271
-
272
- # Split by placeholders to get text parts
273
- text_parts = placeholder_content
274
- for placeholder, var_name in var_placeholders:
275
- text_parts = text_parts.replace(placeholder, f"|{var_name}|")
276
-
277
- # Now split by the pipe delimiters
278
- parts = text_parts.split('|')
279
-
280
- # Build tellraw components
281
- components = []
282
-
283
- for i, part in enumerate(parts):
284
- if i % 2 == 0:
285
- # Text part
286
- if part: # Only add non-empty text parts
287
- components.append(f'{{"text":"{part}"}}')
288
- else:
289
- # Variable part
290
- var_name = part
291
-
292
- # Check if variable has scope selector
293
- if '<' in var_name and var_name.endswith('>'):
294
- # Scoped variable: $variable<selector>$
295
- var_parts = var_name.split('<', 1)
296
- base_var = var_parts[0]
297
- var_selector = var_parts[1][:-1] # Remove trailing >
298
- components.append(f'{{"score":{{"name":"{var_selector}","objective":"{base_var}"}}}}')
299
- else:
300
- # Simple variable: $variable$ - determine selector based on declared scope
301
- var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]" # Default to global
302
- if variable_scopes and var_name in variable_scopes:
303
- declared_scope = variable_scopes[var_name]
304
- if declared_scope == 'global':
305
- var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
306
- else:
307
- var_selector = declared_scope
308
- print(f"DEBUG: Variable {var_name} using declared scope {declared_scope} -> selector {var_selector}")
309
- else:
310
- print(f"DEBUG: Variable {var_name} has no declared scope, using default global selector")
311
-
312
- components.append(f'{{"score":{{"name":"{var_selector}","objective":"{var_name}"}}}}')
313
-
314
- # Join components and create tellraw command
315
- components_str = ','.join(components)
316
- return f'tellraw @a [{components_str}]'
317
-
318
-
319
- def _process_complex_expression(expression: Any, target_selector: str, target_var: str, variable_scopes: Dict[str, str] = None, temp_var_counter: int = 0) -> tuple[List[str], int]:
320
- """
321
- Process complex expressions and break them down into intermediate variables.
322
- Returns (commands, new_temp_counter)
323
- """
324
- if variable_scopes is None:
325
- variable_scopes = {}
326
-
327
- commands = []
328
-
329
- # Handle different expression types
330
- if hasattr(expression, '__class__'):
331
- class_name = str(expression.__class__)
332
-
333
- if 'BinaryExpression' in class_name:
334
- # Binary expression: left operator right
335
- if hasattr(expression, 'left') and hasattr(expression, 'right') and hasattr(expression, 'operator'):
336
- left = expression.left
337
- right = expression.right
338
- operator = expression.operator
339
-
340
- # Process left side
341
- if hasattr(left, 'name'):
342
- # Left is a variable
343
- left_base_name, left_selector = _extract_scope_selector(left.name)
344
- if left_selector == "@s" and variable_scopes and left_base_name in variable_scopes:
345
- left_selector = variable_scopes[left_base_name]
346
- if left_selector == 'global':
347
- left_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
348
-
349
- # Process right side
350
- if hasattr(right, 'value') and isinstance(right.value, (int, str)):
351
- # Right is a literal
352
- if operator == 'PLUS':
353
- commands.append(f"scoreboard players add {target_selector} {target_var} {right.value}")
354
- elif operator == 'MINUS':
355
- commands.append(f"scoreboard players remove {target_selector} {target_var} {right.value}")
356
- elif operator == 'MULTIPLY':
357
- # For multiplication, we need to use operations
358
- temp_var = f"temp_{temp_var_counter}"
359
- temp_var_counter += 1
360
- commands.append(f"scoreboard players set {target_selector} {temp_var} {right.value}")
361
- commands.append(f"scoreboard players operation {target_selector} {target_var} *= {target_selector} {temp_var}")
362
- elif operator == 'DIVIDE':
363
- # For division, we need to use operations
364
- temp_var = f"temp_{temp_var_counter}"
365
- temp_var_counter += 1
366
- commands.append(f"scoreboard players set {target_selector} {temp_var} {right.value}")
367
- commands.append(f"scoreboard players operation {target_selector} {target_var} /= {target_selector} {temp_var}")
368
- else:
369
- # Other operators - use operation
370
- commands.append(f"# Complex operation: {target_var} = {left.name} {operator} {right.value}")
371
- elif hasattr(right, 'name'):
372
- # Right is also a variable - need to use operations
373
- right_base_name, right_selector = _extract_scope_selector(right.name)
374
- if right_selector == "@s" and variable_scopes and right_base_name in variable_scopes:
375
- right_selector = variable_scopes[right_base_name]
376
- if right_selector == 'global':
377
- right_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
378
-
379
- if operator == 'PLUS':
380
- # First set target to left value
381
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {left_selector} {left_base_name}")
382
- # Then add right value
383
- commands.append(f"scoreboard players operation {target_selector} {target_var} += {right_selector} {right_base_name}")
384
- elif operator == 'MINUS':
385
- # First set target to left value
386
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {left_selector} {left_base_name}")
387
- # Then subtract right value
388
- commands.append(f"scoreboard players operation {target_selector} {target_var} -= {right_selector} {right_base_name}")
389
- elif operator == 'MULTIPLY':
390
- # First set target to left value
391
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {left_selector} {left_base_name}")
392
- # Then multiply by right value
393
- commands.append(f"scoreboard players operation {target_selector} {target_var} *= {right_selector} {right_base_name}")
394
- elif operator == 'DIVIDE':
395
- # First set target to left value
396
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {left_selector} {left_base_name}")
397
- # Then divide by right value
398
- commands.append(f"scoreboard players operation {target_selector} {target_var} /= {right_selector} {right_base_name}")
399
- else:
400
- # Other operators - use operation
401
- commands.append(f"# Complex operation: {target_var} = {left.name} {operator} {right.name}")
402
- else:
403
- # Complex right side - need to process recursively
404
- temp_var = f"temp_{temp_var_counter}"
405
- temp_var_counter += 1
406
- right_commands, temp_var_counter = _process_complex_expression(right, target_selector, temp_var, variable_scopes, temp_var_counter)
407
- commands.extend(right_commands)
408
-
409
- # Now perform the operation
410
- if operator == 'PLUS':
411
- commands.append(f"scoreboard players operation {target_selector} {target_var} += {target_selector} {temp_var}")
412
- elif operator == 'MINUS':
413
- commands.append(f"scoreboard players operation {target_selector} {target_var} -= {target_selector} {temp_var}")
414
- elif operator == 'MULTIPLY':
415
- commands.append(f"scoreboard players operation {target_selector} {target_var} *= {target_selector} {temp_var}")
416
- elif operator == 'DIVIDE':
417
- commands.append(f"scoreboard players operation {target_selector} {target_var} /= {target_selector} {temp_var}")
418
- else:
419
- commands.append(f"# Complex operation: {target_var} = {left.name} {operator} {temp_var}")
420
- else:
421
- # Left is not a simple variable - need to process recursively
422
- temp_var = f"temp_{temp_var_counter}"
423
- temp_var_counter += 1
424
- left_commands, temp_var_counter = _process_complex_expression(left, target_selector, temp_var, variable_scopes, temp_var_counter)
425
- commands.extend(left_commands)
426
-
427
- # Now process the right side
428
- if hasattr(right, 'value') and isinstance(right.value, (int, str)):
429
- # Right is a literal
430
- if operator == 'PLUS':
431
- commands.append(f"scoreboard players add {target_selector} {target_var} {right.value}")
432
- elif operator == 'MINUS':
433
- commands.append(f"scoreboard players remove {target_selector} {target_var} {right.value}")
434
- else:
435
- commands.append(f"# Complex operation: {target_var} = {temp_var} {operator} {right.value}")
436
- else:
437
- # Right is also complex - need to process recursively
438
- right_temp_var = f"temp_{temp_var_counter}"
439
- temp_var_counter += 1
440
- right_commands, temp_var_counter = _process_complex_expression(right, target_selector, right_temp_var, variable_scopes, temp_var_counter)
441
- commands.extend(right_commands)
442
-
443
- # Now perform the operation between the two temp variables
444
- if operator == 'PLUS':
445
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {target_selector} {temp_var}")
446
- commands.append(f"scoreboard players operation {target_selector} {target_var} += {target_selector} {right_temp_var}")
447
- elif operator == 'MINUS':
448
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {target_selector} {temp_var}")
449
- commands.append(f"scoreboard players operation {target_selector} {target_var} -= {target_selector} {right_temp_var}")
450
- elif operator == 'MULTIPLY':
451
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {target_selector} {temp_var}")
452
- commands.append(f"scoreboard players operation {target_selector} {target_var} *= {target_selector} {right_temp_var}")
453
- elif operator == 'DIVIDE':
454
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {target_selector} {temp_var}")
455
- commands.append(f"scoreboard players operation {target_selector} {target_var} /= {target_selector} {right_temp_var}")
456
- else:
457
- commands.append(f"# Complex operation: {target_var} = {temp_var} {operator} {right_temp_var}")
458
- else:
459
- commands.append(f"# Malformed binary expression: {expression}")
460
-
461
- elif 'VariableExpression' in class_name:
462
- # Simple variable reference
463
- if hasattr(expression, 'name'):
464
- var_name = expression.name
465
- base_var_name, var_selector = _extract_scope_selector(var_name)
466
- if var_selector == "@s" and variable_scopes and base_var_name in variable_scopes:
467
- var_selector = variable_scopes[base_var_name]
468
- if var_selector == 'global':
469
- var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
470
-
471
- commands.append(f"scoreboard players operation {target_selector} {target_var} = {var_selector} {base_var_name}")
472
- else:
473
- commands.append(f"# Malformed variable expression: {expression}")
474
-
475
- elif 'LiteralExpression' in class_name:
476
- # Literal value
477
- if hasattr(expression, 'value'):
478
- try:
479
- num_value = int(expression.value)
480
- commands.append(f"scoreboard players set {target_selector} {target_var} {num_value}")
481
- except (ValueError, TypeError):
482
- commands.append(f"# Cannot convert literal to number: {expression.value}")
483
- else:
484
- commands.append(f"# Malformed literal expression: {expression}")
485
-
486
- else:
487
- # Unknown expression type
488
- commands.append(f"# Unknown expression type: {class_name} - {expression}")
489
-
490
- else:
491
- # Direct value
492
- try:
493
- num_value = int(expression)
494
- commands.append(f"scoreboard players set {target_selector} {target_var} {num_value}")
495
- except (ValueError, TypeError):
496
- commands.append(f"# Cannot convert expression to number: {expression}")
497
-
498
- return commands, temp_var_counter
499
-
500
-
501
- def _process_statement(statement: Any, namespace: str, function_name: str, statement_index: int = 0, is_tag_function: bool = False, selector: str = "@s", variable_scopes: Dict[str, str] = None, build_context: BuildContext = None, output_dir: Path = None) -> List[str]:
502
- """Process a single statement and return Minecraft commands."""
503
- if variable_scopes is None:
504
- variable_scopes = {}
505
-
506
- if build_context is None:
507
- build_context = BuildContext()
508
-
509
- commands = []
510
-
511
- if statement['type'] == 'command':
512
- command = statement['command']
513
-
514
- # Handle say commands specifically
515
- if command.startswith('say '):
516
- print(f"DEBUG: Found say command: {repr(command)}")
517
- # Convert say command to tellraw command
518
- content = command[4:] # Remove "say " prefix
519
- print(f"DEBUG: Say command content: {repr(content)}")
520
- print(f"DEBUG: Raw command from AST: {repr(statement['command'])}")
521
- # Convert to Minecraft tellraw format
522
- processed_command = _process_say_command_with_variables(content, selector, variable_scopes)
523
- print(f"DEBUG: Processed say command: {repr(processed_command)}")
524
- commands.append(processed_command)
525
- elif command.startswith('tellraw @a ') or command.startswith('tellraw @ a '):
526
- # Fix extra space in tellraw commands
527
- fixed_command = command.replace('tellraw @ a ', 'tellraw @a ')
528
- commands.append(fixed_command)
529
- else:
530
- # Process other commands normally
531
- processed_command = _process_variable_substitutions(command, selector)
532
- commands.append(processed_command)
533
-
534
- elif statement['type'] == 'variable_assignment':
535
- var_name = statement['name']
536
- value = statement['value']
537
-
538
- # Extract scope selector from variable name (e.g., 'player_score<@s>' -> 'player_score', '@s')
539
- base_var_name, var_selector = _extract_scope_selector(var_name)
540
-
541
- # If no scope selector in name, fall back to declared scope
542
- if var_selector == "@s" and variable_scopes and base_var_name in variable_scopes:
543
- declared_scope = variable_scopes[base_var_name]
544
- if declared_scope == 'global':
545
- var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
546
- else:
547
- var_selector = declared_scope
548
-
549
- print(f"DEBUG: Variable {base_var_name} assignment using selector: {var_selector} (from name: {var_name})")
550
-
551
- # Handle different value types
552
- if isinstance(value, int):
553
- commands.append(f"scoreboard players set {var_selector} {base_var_name} {value}")
554
- elif isinstance(value, str) and value.startswith('$') and value.endswith('$'):
555
- # Variable reference
556
- ref_var = value[1:-1] # Remove $ symbols
557
- # Extract scope from reference variable if it has one
558
- ref_base_name, ref_selector = _extract_scope_selector(ref_var)
559
- if ref_selector == "@s" and variable_scopes and ref_base_name in variable_scopes:
560
- declared_scope = variable_scopes[ref_base_name]
561
- if declared_scope == 'global':
562
- ref_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
563
- else:
564
- ref_selector = declared_scope
565
- commands.append(f"scoreboard players operation {var_selector} {base_var_name} = {ref_selector} {ref_base_name}")
566
- elif hasattr(value, '__class__') and 'VariableExpression' in str(value.__class__):
567
- # Variable expression (e.g., playerCounter<@s> = globalCounter<@a>)
568
- if hasattr(value, 'name'):
569
- ref_var = value.name
570
- # Extract scope from reference variable
571
- ref_base_name, ref_selector = _extract_scope_selector(ref_var)
572
- if ref_selector == "@s" and variable_scopes and ref_base_name in variable_scopes:
573
- declared_scope = variable_scopes[ref_base_name]
574
- if declared_scope == 'global':
575
- ref_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
576
- else:
577
- ref_selector = declared_scope
578
- commands.append(f"scoreboard players operation {var_selector} {base_var_name} = {ref_selector} {ref_base_name}")
579
- else:
580
- commands.append(f"# Variable expression assignment: {base_var_name} = {value}")
581
- elif hasattr(value, '__class__') and 'BinaryExpression' in str(value.__class__):
582
- # Handle complex expressions using the enhanced expression processor
583
- # This will break down complex expressions into intermediate variables
584
- expression_commands, _ = _process_complex_expression(value, var_selector, base_var_name, variable_scopes, 0)
585
- commands.extend(expression_commands)
586
- else:
587
- # Handle LiteralExpression and other value types
588
- try:
589
- if hasattr(value, 'value'):
590
- # LiteralExpression case
591
- num_value = int(value.value)
592
- commands.append(f"scoreboard players set {var_selector} {base_var_name} {num_value}")
593
- else:
594
- # Direct value case
595
- num_value = int(value)
596
- commands.append(f"scoreboard players set {var_selector} {base_var_name} {num_value}")
597
- except (ValueError, TypeError):
598
- # If we can't convert to int, add a placeholder
599
- commands.append(f"# Assignment: {base_var_name} = {value}")
600
-
601
- elif statement['type'] == 'if_statement':
602
- condition = statement['condition']
603
- then_body = statement['then_body']
604
- else_body = statement.get('else_body', [])
605
-
606
- # Convert condition to Minecraft syntax
607
- minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector, variable_scopes)
608
-
609
- # Generate unique function names for conditional blocks
610
- if_func_name = f"{function_name}_if_{statement_index}"
611
- else_func_name = f"{function_name}_else_{statement_index}"
612
-
613
- # Create conditional function
614
- if_commands = []
615
- for i, stmt in enumerate(then_body):
616
- if_commands.extend(_process_statement(stmt, namespace, if_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
617
-
618
- # Write conditional function
619
- if if_commands:
620
- # Use the output directory parameter
621
- if output_dir:
622
- if_dir = output_dir / "data" / namespace / "function"
623
- else:
624
- if_dir = Path(f"data/{namespace}/function")
625
- ensure_dir(str(if_dir))
626
- with open(if_dir / f"{if_func_name}.mcfunction", 'w', encoding='utf-8') as f:
627
- f.write('\n'.join(if_commands))
628
-
629
- # Create else function if needed
630
- if else_body:
631
- else_commands = []
632
- for i, stmt in enumerate(else_body):
633
- else_commands.extend(_process_statement(stmt, namespace, else_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
634
-
635
- if else_commands:
636
- with open(if_dir / f"{else_func_name}.mcfunction", 'w', encoding='utf-8') as f:
637
- f.write('\n'.join(else_commands))
638
-
639
- # Add the conditional execution command
640
- if else_body:
641
- commands.append(f"execute if {minecraft_condition} run function {namespace}:{if_func_name}")
642
- commands.append(f"execute unless {minecraft_condition} run function {namespace}:{else_func_name}")
643
- # Add if_end function call for cleanup
644
- commands.append(f"function {namespace}:{function_name}_if_end_{statement_index}")
645
- else:
646
- commands.append(f"execute if {minecraft_condition} run function {namespace}:{if_func_name}")
647
-
648
- elif statement['type'] == 'while_loop' or statement['type'] == 'while_statement':
649
- # Handle while loops using recursion
650
- loop_commands = _process_while_loop_recursion(statement, namespace, function_name, statement_index, is_tag_function, selector, variable_scopes, build_context, output_dir)
651
- commands.extend(loop_commands)
652
-
653
- elif statement['type'] == 'function_call':
654
- func_name = statement['name']
655
- scope = statement.get('scope')
656
- func_namespace = statement.get('namespace', namespace) # Use specified namespace or current namespace
657
-
658
- if scope:
659
- # Handle scoped function call
660
- if scope == 'global':
661
- # Global scope uses the server armor stand
662
- selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
663
- else:
664
- # Use the specified scope selector
665
- selector = scope
666
-
667
- # Generate execute as command
668
- commands.append(f"execute as {selector} run function {func_namespace}:{func_name}")
669
- else:
670
- # Simple function call without scope
671
- commands.append(f"function {func_namespace}:{func_name}")
672
-
673
- elif statement['type'] == 'raw_text':
674
- # Raw Minecraft commands
675
- raw_commands = statement['commands']
676
- for cmd in raw_commands:
677
- processed_cmd = _process_variable_substitutions(cmd, selector)
678
- commands.append(processed_cmd)
679
-
680
- return commands
681
-
682
-
683
- def _generate_function_file(ast: Dict[str, Any], output_dir: Path, namespace: str, verbose: bool = False, build_context: BuildContext = None) -> None:
684
- """Generate function files from the AST for a specific namespace."""
685
- if build_context is None:
686
- build_context = BuildContext()
687
-
688
- if 'functions' not in ast:
689
- return
690
-
691
- # Filter functions by namespace
692
- namespace_functions = []
693
- for func in ast['functions']:
694
- # Check if function belongs to this namespace
695
- # For now, we'll generate all functions in all namespaces
696
- # In the future, we could add namespace annotations to functions
697
- namespace_functions.append(func)
698
-
699
- if verbose:
700
- print(f"DEBUG: Processing {len(namespace_functions)} functions for namespace {namespace}")
701
-
702
- for func in namespace_functions:
703
- func_name = func['name']
704
- func_body = func.get('body', [])
705
-
706
- # Collect variable scopes from the AST
707
- variable_scopes = {}
708
- if 'variables' in ast:
709
- for var_decl in ast['variables']:
710
- var_name = var_decl['name']
711
- var_scope = var_decl.get('scope')
712
-
713
- # Extract base variable name and scope selector
714
- base_var_name, scope_selector = _extract_scope_selector(var_name)
715
-
716
- if scope_selector != "@s":
717
- # Variable has explicit scope selector
718
- variable_scopes[base_var_name] = scope_selector
719
- print(f"DEBUG: Variable {base_var_name} has scope {scope_selector} from name {var_name}")
720
- elif var_scope:
721
- # Variable has scope from scope field (legacy support)
722
- variable_scopes[base_var_name] = var_scope
723
- print(f"DEBUG: Variable {base_var_name} has scope {var_scope} from scope field")
724
- else:
725
- # Variable has no scope (defaults to @s)
726
- print(f"DEBUG: Variable {base_var_name} has no scope (defaults to @s)")
727
-
728
- print(f"DEBUG: Collected variable scopes: {variable_scopes}")
729
-
730
- # Generate function content
731
- function_commands = []
732
- for i, statement in enumerate(func_body):
733
- try:
734
- print(f"DEBUG: Processing statement {i} of type {statement.get('type', 'unknown')}: {statement}")
735
- commands = _process_statement(statement, namespace, func_name, i, False, "@s", variable_scopes, build_context, output_dir)
736
- function_commands.extend(commands)
737
- print(f"Generated {len(commands)} commands for statement {i} in function {func_name}: {commands}")
738
- except Exception as e:
739
- print(f"Warning: Error processing statement {i} in function {func_name}: {e}")
740
- import traceback
741
- traceback.print_exc()
742
- continue
743
-
744
- # Write function file
745
- if function_commands:
746
- # Only add armor stand setup to main functions that need it
747
- # Don't add to helper functions or functions in the "other" namespace
748
- should_add_armor_stand = (namespace != "other" and
749
- (func_name in ["main", "init", "load"] or
750
- any(cmd for cmd in function_commands if "scoreboard" in cmd or "tellraw" in cmd)))
751
-
752
- final_commands = []
753
- if should_add_armor_stand:
754
- final_commands.append("execute unless entity @e[type=armor_stand,tag=mdl_server,limit=1] run summon armor_stand ~ 320 ~ {Tags:[\"mdl_server\"],Invisible:1b,Marker:1b,NoGravity:1b,Invulnerable:1b}")
755
- final_commands.extend(function_commands)
756
-
757
- if verbose:
758
- print(f"DEBUG: Final commands for {func_name}: {final_commands}")
759
-
760
- func_dir = output_dir / "data" / namespace / "function"
761
- ensure_dir(str(func_dir))
762
-
763
- with open(func_dir / f"{func_name}.mcfunction", 'w', encoding='utf-8') as f:
764
- content = '\n'.join(final_commands)
765
- if verbose:
766
- print(f"DEBUG: Writing to file {func_name}.mcfunction: {repr(content)}")
767
- f.write(content)
768
-
769
- if verbose:
770
- print(f"Generated function: {namespace}:{func_name}")
771
- else:
772
- if verbose:
773
- print(f"No commands generated for function: {namespace}:{func_name}")
774
- print(f"Function body: {func_body}")
775
-
776
-
777
- def _generate_hook_files(ast: Dict[str, Any], output_dir: Path, namespace: str, build_context: BuildContext = None, all_namespaces: List[str] = None) -> None:
778
- """Generate load and tick tag files."""
779
- if build_context is None:
780
- build_context = BuildContext()
781
-
782
- if all_namespaces is None:
783
- all_namespaces = [namespace]
784
-
785
- # Generate load tag
786
- load_tag_dir = output_dir / "data" / "minecraft" / "tags" / "function"
787
- ensure_dir(str(load_tag_dir))
788
-
789
- # Start with load functions for all namespaces
790
- load_values = []
791
- for ns in all_namespaces:
792
- load_values.append(f"{ns}:load")
793
-
794
- # Add functions specified in on_load hooks
795
- if 'hooks' in ast:
796
- for hook in ast['hooks']:
797
- if hook['hook_type'] == 'load':
798
- load_values.append(hook['function_name'])
799
-
800
- # Add pack-specific load function if pack name is available and different from namespace
801
- if 'pack' in ast and 'name' in ast['pack']:
802
- pack_name = ast['pack']['name']
803
- pack_load_function = f"{pack_name}:load"
804
- # Only add if it's not already in the list (avoids duplicates when pack name == namespace)
805
- if pack_load_function not in load_values:
806
- load_values.append(pack_load_function)
807
-
808
- # Remove duplicates while preserving order
809
- seen = set()
810
- unique_load_values = []
811
- for value in load_values:
812
- if value not in seen:
813
- seen.add(value)
814
- unique_load_values.append(value)
815
-
816
- load_tag_content = {
817
- "values": unique_load_values
818
- }
819
- write_json(str(load_tag_dir / "load.json"), load_tag_content)
820
-
821
- # Generate tick tag if there are tick functions
822
- tick_functions = []
823
- if 'functions' in ast:
824
- for func in ast['functions']:
825
- if func.get('name', '').startswith('tick'):
826
- tick_functions.append(f"{namespace}:{func['name']}")
827
-
828
- if tick_functions:
829
- # Remove duplicates while preserving order
830
- seen = set()
831
- unique_tick_functions = []
832
- for value in tick_functions:
833
- if value not in seen:
834
- seen.add(value)
835
- unique_tick_functions.append(value)
836
-
837
- tick_tag_content = {
838
- "values": unique_tick_functions
839
- }
840
- write_json(str(load_tag_dir / "tick.json"), tick_tag_content)
841
-
842
-
843
- def _generate_global_load_function(ast: Dict[str, Any], output_dir: Path, namespace: str, build_context: BuildContext = None) -> None:
844
- """Generate the global load function."""
845
- if build_context is None:
846
- build_context = BuildContext()
847
-
848
- # Generate scoreboard objectives
849
- scoreboard_commands = _generate_scoreboard_objectives(ast, output_dir)
850
-
851
- # Generate load function
852
- _generate_load_function(scoreboard_commands, output_dir, namespace, ast)
853
-
854
-
855
- def _generate_tag_files(ast: Dict[str, Any], output_dir: Path, namespace: str) -> None:
856
- """Generate tag files for the datapack."""
857
- # This is handled by _generate_hook_files
858
- pass
859
-
860
-
861
- def _validate_pack_format(pack_format: int) -> None:
862
- """Validate the pack format number."""
863
- if not isinstance(pack_format, int) or pack_format < 1:
864
- raise ValueError(f"Invalid pack format: {pack_format}. Must be a positive integer.")
865
-
866
-
867
- def _collect_conditional_functions(if_statement, namespace: str, function_name: str, statement_index: int, is_tag_function: bool = False, selector: str = "@s", variable_scopes: Dict[str, str] = None, build_context: BuildContext = None) -> List[str]:
868
- """Collect conditional functions from if statements."""
869
- if variable_scopes is None:
870
- variable_scopes = {}
871
-
872
- if build_context is None:
873
- build_context = BuildContext()
874
-
875
- conditional_functions = []
876
-
877
- # Generate function name for this conditional
878
- if_func_name = f"{function_name}_if_{statement_index}"
879
- conditional_functions.append(if_func_name)
880
-
881
- # Process then body
882
- for i, stmt in enumerate(if_statement['then_body']):
883
- if stmt['type'] == 'if_statement':
884
- nested_functions = _collect_conditional_functions(stmt, namespace, if_func_name, i, is_tag_function, selector, variable_scopes, build_context)
885
- conditional_functions.extend(nested_functions)
886
-
887
- # Process else body if it exists
888
- if 'else_body' in if_statement and if_statement['else_body']:
889
- else_func_name = f"{function_name}_else_{statement_index}"
890
- conditional_functions.append(else_func_name)
891
-
892
- for i, stmt in enumerate(if_statement['else_body']):
893
- if stmt['type'] == 'if_statement':
894
- nested_functions = _collect_conditional_functions(stmt, namespace, else_func_name, i, is_tag_function, selector, variable_scopes, build_context)
895
- conditional_functions.extend(nested_functions)
896
-
897
- return conditional_functions
898
-
899
-
900
- def _process_while_loop_recursion(while_statement, namespace: str, function_name: str, statement_index: int, is_tag_function: bool = False, selector: str = "@s", variable_scopes: Dict[str, str] = None, build_context: BuildContext = None, output_dir: Path = None) -> List[str]:
901
- """Process while loops using recursive function calls."""
902
- if variable_scopes is None:
903
- variable_scopes = {}
904
-
905
- if build_context is None:
906
- build_context = BuildContext()
907
-
908
- condition = while_statement['condition']
909
- body = while_statement['body']
910
-
911
- # Generate unique function names - they should be the same according to the test
912
- loop_func_name = f"test_{function_name}_while_{statement_index}"
913
-
914
- # Process loop body
915
- body_commands = []
916
- for i, stmt in enumerate(body):
917
- body_commands.extend(_process_statement(stmt, namespace, loop_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
918
-
919
- # Add the recursive call to the loop body
920
- minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector, variable_scopes)
921
- body_commands.append(f"execute if {minecraft_condition} run function {namespace}:{loop_func_name}")
922
-
923
- # Write the single loop function
924
- if body_commands:
925
- # Use the output directory parameter
926
- if output_dir:
927
- func_dir = output_dir / "data" / namespace / "function"
928
- else:
929
- func_dir = Path(f"data/{namespace}/function")
930
- ensure_dir(str(func_dir))
931
- with open(func_dir / f"{loop_func_name}.mcfunction", 'w', encoding='utf-8') as f:
932
- f.write('\n'.join(body_commands))
933
-
934
- # Return the command to start the loop with conditional execution
935
- return [f"execute if {minecraft_condition} run function {namespace}:{loop_func_name}"]
936
-
937
-
938
- def _process_while_loop_schedule(while_statement, namespace: str, function_name: str, statement_index: int, is_tag_function: bool = False, selector: str = "@s", variable_scopes: Dict[str, str] = None, build_context: BuildContext = None) -> List[str]:
939
- """Process while loops using scheduled functions."""
940
- if variable_scopes is None:
941
- variable_scopes = {}
942
-
943
- if build_context is None:
944
- build_context = BuildContext()
945
-
946
- condition = while_statement['condition']
947
- body = while_statement['body']
948
-
949
- # Generate unique function names
950
- loop_func_name = f"{function_name}_while_{statement_index}"
951
- loop_body_func_name = f"{function_name}_while_body_{statement_index}"
952
-
953
- # Process loop body
954
- body_commands = []
955
- for i, stmt in enumerate(body):
956
- body_commands.extend(_process_statement(stmt, namespace, loop_body_func_name, i, is_tag_function, selector, variable_scopes, build_context))
957
-
958
- # Add the loop continuation command
959
- minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector, variable_scopes)
960
- body_commands.append(f"execute {minecraft_condition} run schedule function {namespace}:{loop_body_func_name} 1t")
961
-
962
- # Write loop body function
963
- if body_commands:
964
- # Use the output directory from build context
965
- if hasattr(build_context, 'output_dir'):
966
- func_dir = build_context.output_dir / "data" / namespace / "function"
967
- else:
968
- func_dir = Path(f"data/{namespace}/function")
969
- ensure_dir(str(func_dir))
970
- with open(func_dir / f"{loop_body_func_name}.mcfunction", 'w', encoding='utf-8') as f:
971
- f.write('\n'.join(body_commands))
972
-
973
- # Return the command to start the loop
974
- return [f"schedule function {namespace}:{loop_body_func_name} 1t"]
975
-
976
-
977
- def _create_zip_file(source_dir: Path, zip_path: Path) -> None:
978
- """Create a zip file from a directory."""
979
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
980
- for file_path in source_dir.rglob('*'):
981
- if file_path.is_file():
982
- arcname = file_path.relative_to(source_dir)
983
- zipf.write(file_path, arcname)
984
-
985
-
986
- def _generate_pack_mcmeta(ast: Dict[str, Any], output_dir: Path) -> None:
987
- """Generate the pack.mcmeta file."""
988
- pack_format = ast.get('pack', {}).get('format', 82)
989
- pack_description = ast.get('pack', {}).get('description', 'MDL Generated Datapack')
990
-
991
- pack_meta = {
992
- "pack": {
993
- "pack_format": pack_format,
994
- "description": pack_description
995
- }
996
- }
997
-
998
- write_json(str(output_dir / "pack.mcmeta"), pack_meta)
999
-
1000
-
1001
- def _ast_to_pack(ast: Dict[str, Any], mdl_files: List[Path]) -> Pack:
1002
- """Convert AST to Pack object."""
1003
- pack_info = ast.get('pack', {})
1004
- if pack_info is None:
1005
- pack_info = {}
1006
- pack_name = pack_info.get('name', 'mdl_pack')
1007
- pack_format = pack_info.get('pack_format', 82) # Use pack_format instead of format
1008
- pack_description = pack_info.get('description', 'MDL Generated Datapack')
1009
-
1010
- pack = Pack(pack_name, pack_description, pack_format)
1011
-
1012
- # Add namespaces and functions
1013
- if 'functions' in ast:
1014
- # Get namespace name from AST or use pack name
1015
- namespace_info = ast.get('namespace', {})
1016
- if namespace_info is None:
1017
- namespace_info = {}
1018
- namespace_name = namespace_info.get('name', pack_name)
1019
- namespace = pack.namespace(namespace_name)
1020
-
1021
- for func in ast['functions']:
1022
- function_name = func['name']
1023
- # Create function and add commands if they exist
1024
- function = namespace.function(function_name)
1025
-
1026
- # Add commands from function body if they exist
1027
- if 'body' in func:
1028
- for i, statement in enumerate(func['body']):
1029
- try:
1030
- # Use the same processing logic as the build system
1031
- commands = _process_statement(statement, namespace_name, function_name, i, False, "@s", {}, BuildContext())
1032
- function.commands.extend(commands)
1033
- except Exception as e:
1034
- # If processing fails, try to add as simple command
1035
- if statement.get('type') == 'command':
1036
- function.commands.append(statement['command'])
1037
- elif statement.get('type') == 'function_call':
1038
- func_name = statement['name']
1039
- scope = statement.get('scope')
1040
- func_namespace = statement.get('namespace', namespace_name) # Use specified namespace or current namespace
1041
-
1042
- if scope:
1043
- # Handle scoped function call
1044
- if scope == 'global':
1045
- # Global scope uses the server armor stand
1046
- selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
1047
- else:
1048
- # Use the specified scope selector
1049
- selector = scope
1050
-
1051
- # Generate execute as command
1052
- function.commands.append(f"execute as {selector} run function {func_namespace}:{func_name}")
1053
- else:
1054
- # Simple function call without scope
1055
- function.commands.append(f"function {func_namespace}:{func_name}")
1056
- elif statement.get('type') == 'variable_assignment':
1057
- # Handle variable assignments
1058
- var_name = statement['name']
1059
- value = statement['value']
1060
-
1061
- # Determine selector based on variable scope
1062
- var_selector = "@s" # Default
1063
- if 'variables' in ast:
1064
- for var_decl in ast['variables']:
1065
- if var_decl.get('name') == var_name:
1066
- var_scope = var_decl.get('scope')
1067
- if var_scope == 'global':
1068
- var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
1069
- elif var_scope:
1070
- var_selector = var_scope
1071
- break
1072
-
1073
- if hasattr(value, 'value'):
1074
- # Simple literal value
1075
- function.commands.append(f"scoreboard players set {var_name} {var_selector} {value.value}")
1076
- else:
1077
- # Complex expression - add a placeholder
1078
- function.commands.append(f"# Variable assignment: {var_name} = {value}")
1079
- else:
1080
- # Add a placeholder for other statement types
1081
- function.commands.append(f"# Statement: {statement.get('type', 'unknown')}")
1082
-
1083
- # Add variables
1084
- if 'variables' in ast:
1085
- for var in ast['variables']:
1086
- # Variables are handled during command processing
1087
- pass
1088
-
1089
- # Add hooks
1090
- if 'hooks' in ast:
1091
- for hook in ast['hooks']:
1092
- if hook['hook_type'] == 'load':
1093
- pack.on_load(hook['function_name'])
1094
- elif hook['hook_type'] == 'tick':
1095
- pack.on_tick(hook['function_name'])
1096
-
1097
- # Add recipes
1098
- if 'recipes' in ast:
1099
- namespace_info = ast.get('namespace', {})
1100
- if namespace_info is None:
1101
- namespace_info = {}
1102
- namespace_name = namespace_info.get('name', pack_name)
1103
- namespace = pack.namespace(namespace_name)
1104
-
1105
- for recipe in ast['recipes']:
1106
- recipe_name = recipe['name']
1107
- recipe_data = recipe['data']
1108
- # Create recipe object
1109
- from .pack import Recipe
1110
- recipe_obj = Recipe(recipe_name, recipe_data)
1111
- namespace.recipes[recipe_name] = recipe_obj
1112
-
1113
- # Add advancements
1114
- if 'advancements' in ast:
1115
- namespace_info = ast.get('namespace', {})
1116
- if namespace_info is None:
1117
- namespace_info = {}
1118
- namespace_name = namespace_info.get('name', pack_name)
1119
- namespace = pack.namespace(namespace_name)
1120
-
1121
- for advancement in ast['advancements']:
1122
- advancement_name = advancement['name']
1123
- advancement_data = advancement['data']
1124
- # Create advancement object
1125
- from .pack import Advancement
1126
- advancement_obj = Advancement(advancement_name, advancement_data)
1127
- namespace.advancements[advancement_name] = advancement_obj
1128
-
1129
- # Add loot tables
1130
- if 'loot_tables' in ast:
1131
- namespace_info = ast.get('namespace', {})
1132
- if namespace_info is None:
1133
- namespace_info = {}
1134
- namespace_name = namespace_info.get('name', pack_name)
1135
- namespace = pack.namespace(namespace_name)
1136
-
1137
- for loot_table in ast['loot_tables']:
1138
- loot_table_name = loot_table['name']
1139
- loot_table_data = loot_table['data']
1140
- # Create loot table object
1141
- from .pack import LootTable
1142
- loot_table_obj = LootTable(loot_table_name, loot_table_data)
1143
- namespace.loot_tables[loot_table_name] = loot_table_obj
1144
-
1145
- return pack
1146
-
1147
-
1148
- def build_mdl(input_path: str, output_path: str, verbose: bool = False, pack_format_override: Optional[int] = None, wrapper: Optional[str] = None, ignore_warnings: bool = False) -> None:
1149
- """Build MDL files into a Minecraft datapack."""
1150
- error_collector = MDLErrorCollector()
1151
-
1152
- try:
1153
- input_dir = Path(input_path)
1154
- output_dir = Path(output_path)
1155
-
1156
- # Validate input directory exists
1157
- if not input_dir.exists():
1158
- error_collector.add_error(create_error(
1159
- MDLFileError,
1160
- f"Input path does not exist: {input_path}",
1161
- file_path=input_path,
1162
- suggestion="Check the path and ensure the file or directory exists."
1163
- ))
1164
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1165
- error_collector.raise_if_errors()
1166
- return
1167
-
1168
- # Find MDL files
1169
- mdl_files = _find_mdl_files(input_dir)
1170
-
1171
- if not mdl_files:
1172
- error_collector.add_error(create_error(
1173
- MDLFileError,
1174
- f"No .mdl files found in {input_path}",
1175
- file_path=input_path,
1176
- suggestion="Ensure the directory contains .mdl files or specify a single .mdl file."
1177
- ))
1178
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1179
- error_collector.raise_if_errors()
1180
- return
1181
-
1182
- if verbose:
1183
- print_section("Building MDL Project")
1184
- print_info(f"Found {len(mdl_files)} MDL file(s):")
1185
- for file in mdl_files:
1186
- print_info(f" - {color.file_path(str(file))}")
1187
- print_separator()
1188
-
1189
- # Merge and parse MDL files
1190
- ast = _merge_mdl_files(mdl_files, verbose, error_collector)
1191
-
1192
- if ast is None:
1193
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1194
- error_collector.raise_if_errors()
1195
- return
1196
-
1197
- # Override pack format if specified
1198
- if pack_format_override is not None:
1199
- _validate_pack_format(pack_format_override)
1200
- if 'pack' not in ast:
1201
- ast['pack'] = {}
1202
- ast['pack']['format'] = pack_format_override
1203
-
1204
- # Create output directory
1205
- ensure_dir(str(output_dir))
1206
-
1207
- # Generate pack.mcmeta
1208
- _generate_pack_mcmeta(ast, output_dir)
1209
-
1210
- # Handle multiple namespaces from multiple files
1211
- namespaces = []
1212
-
1213
- # Get namespaces from AST
1214
- if 'namespaces' in ast:
1215
- for ns in ast['namespaces']:
1216
- if 'name' in ns:
1217
- namespaces.append(_slugify(ns['name']))
1218
-
1219
- # If no explicit namespaces, use the pack name as default
1220
- if not namespaces:
1221
- default_namespace = ast.get('pack', {}).get('name', 'mdl_pack')
1222
- namespaces.append(_slugify(default_namespace))
1223
-
1224
- # Debug: Show what namespaces we're using
1225
- if verbose:
1226
- print_info(f"AST namespaces: {ast.get('namespaces', [])}")
1227
- print_info(f"AST pack: {ast.get('pack', {})}")
1228
- print_info(f"Using namespaces: {namespaces}")
1229
-
1230
- # Generate functions for each namespace
1231
- build_context = BuildContext()
1232
- for namespace in namespaces:
1233
- if verbose:
1234
- print_info(f"Processing namespace: {color.highlight(namespace)}")
1235
- _generate_function_file(ast, output_dir, namespace, verbose, build_context)
1236
-
1237
- # Generate hook files (load/tick tags) - include all namespaces
1238
- primary_namespace = namespaces[0] if namespaces else 'mdl_pack'
1239
- _generate_hook_files(ast, output_dir, primary_namespace, build_context, all_namespaces=namespaces)
1240
-
1241
- # Generate load functions for all namespaces
1242
- for namespace in namespaces:
1243
- _generate_global_load_function(ast, output_dir, namespace, build_context)
1244
-
1245
- # Create zip file (always create one, use wrapper name if specified)
1246
- zip_name = wrapper if wrapper else output_dir.name
1247
- # When using wrapper, create zip in output directory; otherwise in parent directory
1248
- if wrapper:
1249
- zip_path = output_dir / f"{zip_name}.zip"
1250
- else:
1251
- zip_path = output_dir.parent / f"{zip_name}.zip"
1252
- _create_zip_file(output_dir, zip_path)
1253
- if verbose:
1254
- print_info(f"Created zip file: {color.file_path(str(zip_path))}")
1255
-
1256
- print_success(f"Successfully built datapack: {color.file_path(output_path)}")
1257
- if verbose:
1258
- print_info(f"Output directory: {color.file_path(str(output_dir))}")
1259
- print_info(f"Namespace: {color.highlight(namespace)}")
1260
-
1261
- except MDLLexerError as e:
1262
- error_collector.add_error(e)
1263
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1264
- error_collector.raise_if_errors()
1265
- except MDLParserError as e:
1266
- error_collector.add_error(e)
1267
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1268
- error_collector.raise_if_errors()
1269
- except MDLSyntaxError as e:
1270
- error_collector.add_error(e)
1271
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1272
- error_collector.raise_if_errors()
1273
- except MDLBuildError as e:
1274
- error_collector.add_error(e)
1275
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1276
- error_collector.raise_if_errors()
1277
- except MDLFileError as e:
1278
- error_collector.add_error(e)
1279
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1280
- error_collector.raise_if_errors()
1281
- except MDLCompilationError as e:
1282
- error_collector.add_error(e)
1283
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1284
- error_collector.raise_if_errors()
1285
- except Exception as e:
1286
- error_collector.add_error(create_error(
1287
- MDLBuildError,
1288
- f"Unexpected error during build: {str(e)}",
1289
- suggestion="Check the input files and try again. If the problem persists, report this as a bug."
1290
- ))
1291
- error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1292
- error_collector.raise_if_errors()