minecraft-datapack-language 15.4.6__py3-none-any.whl → 15.4.7__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.
@@ -1,250 +1,1080 @@
1
1
  """
2
- CLI Build Module - Handles building MDL files into Minecraft datapacks
2
+ CLI Build Functions - Core build functionality for MDL CLI
3
3
  """
4
4
 
5
5
  import os
6
6
  import shutil
7
7
  import zipfile
8
8
  from pathlib import Path
9
- from typing import Optional, List
9
+ from typing import Dict, List, Optional, Any
10
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
11
18
  from .cli_colors import (
12
19
  print_success, print_warning, print_error, print_info,
13
20
  print_section, print_separator, color
14
21
  )
15
- from .mdl_errors import MDLErrorCollector, create_error, MDLConfigurationError, MDLFileError
16
- from .pack import Pack
17
- from .utils import find_mdl_files
18
22
 
19
23
 
20
- def _ast_to_pack(ast, mdl_files):
21
- """Convert AST to Pack object - placeholder implementation"""
22
- # This is a simplified implementation - the original may have been more complex
23
- pack = Pack("default", "Default pack", 48)
24
- # TODO: Implement proper AST to Pack conversion
25
- return pack
24
+ class BuildContext:
25
+ """Context for build operations to prevent race conditions."""
26
+
27
+ def __init__(self):
28
+ self.conditional_functions = []
29
+ self.variable_scopes = {}
30
+ self.namespace_functions = {}
31
+ self.expression_processor = ExpressionProcessor()
26
32
 
27
33
 
28
- def _merge_mdl_files(mdl_files):
29
- """Merge multiple MDL files - placeholder implementation"""
30
- # This is a simplified implementation - the original may have been more complex
31
- pack = Pack("merged", "Merged pack", 48)
32
- # TODO: Implement proper MDL file merging
33
- return pack
34
+ def _merge_mdl_files(files: List[Path], verbose: bool = False, error_collector: MDLErrorCollector = None) -> Optional[Dict[str, Any]]:
35
+ """Merge multiple MDL files into a single AST."""
36
+ if not files:
37
+ return None
38
+
39
+ # Parse the first file to get the base AST
40
+ try:
41
+ if verbose:
42
+ print(f"DEBUG: Parsing first file: {files[0]}")
43
+
44
+ with open(files[0], 'r', encoding='utf-8') as f:
45
+ content = f.read()
46
+
47
+ ast = parse_mdl_js(content, str(files[0]))
48
+
49
+ if verbose:
50
+ print(f"DEBUG: Successfully parsed {files[0]}")
51
+
52
+ except MDLLexerError as e:
53
+ if error_collector:
54
+ error_collector.add_error(e)
55
+ else:
56
+ raise
57
+ return None
58
+ except MDLParserError as e:
59
+ if error_collector:
60
+ error_collector.add_error(e)
61
+ else:
62
+ raise
63
+ return None
64
+ except MDLSyntaxError as e:
65
+ if error_collector:
66
+ error_collector.add_error(e)
67
+ else:
68
+ raise
69
+ return None
70
+ except Exception as e:
71
+ if error_collector:
72
+ error_collector.add_error(create_error(
73
+ MDLCompilationError,
74
+ f"Failed to parse {files[0]}: {str(e)}",
75
+ file_path=str(files[0]),
76
+ suggestion="Check the file syntax and ensure it's a valid MDL file."
77
+ ))
78
+ else:
79
+ raise
80
+ return None
81
+
82
+ # Merge additional files
83
+ for file_path in files[1:]:
84
+ try:
85
+ if verbose:
86
+ print(f"DEBUG: Parsing additional file: {file_path}")
87
+
88
+ with open(file_path, 'r', encoding='utf-8') as f:
89
+ content = f.read()
90
+
91
+ additional_ast = parse_mdl_js(content, str(file_path))
92
+
93
+ if verbose:
94
+ print(f"DEBUG: Successfully parsed {file_path}")
95
+
96
+ # Merge functions
97
+ if 'functions' in additional_ast:
98
+ if 'functions' not in ast:
99
+ ast['functions'] = []
100
+ ast['functions'].extend(additional_ast['functions'])
101
+
102
+ # Merge variables
103
+ if 'variables' in additional_ast:
104
+ if 'variables' not in ast:
105
+ ast['variables'] = []
106
+ ast['variables'].extend(additional_ast['variables'])
107
+
108
+ # Merge namespaces (append to existing list)
109
+ if 'namespaces' in additional_ast:
110
+ if 'namespaces' not in ast:
111
+ ast['namespaces'] = []
112
+ ast['namespaces'].extend(additional_ast['namespaces'])
113
+
114
+ # Merge registry declarations
115
+ for registry_type in ['recipes', 'loot_tables', 'advancements', 'predicates', 'item_modifiers', 'structures']:
116
+ if registry_type in additional_ast:
117
+ if registry_type not in ast:
118
+ ast[registry_type] = []
119
+ ast[registry_type].extend(additional_ast[registry_type])
120
+
121
+ # Merge pack metadata (use the first one found)
122
+ if 'pack' in additional_ast and 'pack' not in ast:
123
+ ast['pack'] = additional_ast['pack']
124
+
125
+ except MDLLexerError as e:
126
+ if error_collector:
127
+ error_collector.add_error(e)
128
+ else:
129
+ raise
130
+ return None
131
+ except MDLParserError as e:
132
+ if error_collector:
133
+ error_collector.add_error(e)
134
+ else:
135
+ raise
136
+ return None
137
+ except MDLSyntaxError as e:
138
+ if error_collector:
139
+ error_collector.add_error(e)
140
+ else:
141
+ raise
142
+ return None
143
+ except Exception as e:
144
+ if error_collector:
145
+ error_collector.add_error(create_error(
146
+ MDLCompilationError,
147
+ f"Failed to parse {file_path}: {str(e)}",
148
+ file_path=str(file_path),
149
+ suggestion="Check the file syntax and ensure it's a valid MDL file."
150
+ ))
151
+ else:
152
+ raise
153
+ return None
154
+
155
+ return ast
34
156
 
35
157
 
36
- def build_mdl(input_path: str, output_path: str, verbose: bool = False,
37
- pack_format_override: Optional[int] = None, wrapper: Optional[str] = None,
38
- ignore_warnings: bool = False) -> None:
39
- """Build MDL files into a Minecraft datapack.
158
+ def _generate_scoreboard_objectives(ast: Dict[str, Any], output_dir: Path) -> List[str]:
159
+ """Generate scoreboard objectives for all variables."""
160
+ scoreboard_commands = []
40
161
 
41
- Args:
42
- input_path: Path to MDL file or directory
43
- output_path: Output directory for the datapack
44
- verbose: Enable verbose output
45
- pack_format_override: Override pack format number
46
- wrapper: Create zip file with specified name
47
- ignore_warnings: Suppress warning messages
48
- """
49
- error_collector = MDLErrorCollector()
162
+ # Collect all variable names in order of appearance
163
+ variables = []
164
+ seen_variables = set()
50
165
 
51
- try:
52
- # Validate input path
53
- input_path_obj = Path(input_path)
54
- if not input_path_obj.exists():
55
- raise MDLFileError(
56
- message=f"Input path does not exist: {input_path}",
57
- suggestion="Check the path and try again."
58
- )
166
+ # From variable declarations (preserve order)
167
+ if 'variables' in ast:
168
+ for var_decl in ast['variables']:
169
+ if 'name' in var_decl and var_decl['name'] not in seen_variables:
170
+ variables.append(var_decl['name'])
171
+ seen_variables.add(var_decl['name'])
172
+
173
+ # From functions (scan for variable usage)
174
+ if 'functions' in ast:
175
+ for func in ast['functions']:
176
+ if 'body' in func:
177
+ for statement in func['body']:
178
+ # Look for variable assignments and usage
179
+ if statement['type'] == 'variable_assignment':
180
+ if statement['name'] not in seen_variables:
181
+ variables.append(statement['name'])
182
+ seen_variables.add(statement['name'])
183
+ elif statement['type'] == 'command':
184
+ # Scan command for variable substitutions
185
+ command = statement['command']
186
+ import re
187
+ var_matches = re.findall(r'\$([^$]+)\$', command)
188
+ for var_name in var_matches:
189
+ # Extract base name from scoped variables
190
+ base_name = _extract_base_variable_name(var_name)
191
+ if base_name not in seen_variables:
192
+ variables.append(base_name)
193
+ seen_variables.add(base_name)
194
+
195
+ # Create scoreboard objectives in the order they were found
196
+ for var_name in variables:
197
+ scoreboard_commands.append(f"scoreboard objectives add {var_name} dummy")
198
+
199
+ return scoreboard_commands
200
+
201
+
202
+ def _generate_load_function(scoreboard_commands: List[str], output_dir: Path, namespace: str, ast: Dict[str, Any]) -> None:
203
+ """Generate the load function with scoreboard setup."""
204
+ load_content = []
205
+
206
+ # Add armor stand setup for server-side operations
207
+ 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}")
208
+
209
+ # Add scoreboard objectives
210
+ load_content.extend(scoreboard_commands)
211
+
212
+ # Add any custom load commands from the AST
213
+ if 'load' in ast:
214
+ for command in ast['load']:
215
+ load_content.append(command)
216
+
217
+ # Write the load function
218
+ load_dir = output_dir / "data" / namespace / "function"
219
+ ensure_dir(str(load_dir))
220
+
221
+ with open(load_dir / "load.mcfunction", 'w', encoding='utf-8') as f:
222
+ f.write('\n'.join(load_content))
223
+
224
+
225
+ def _process_say_command_with_variables(content: str, selector: str, variable_scopes: Dict[str, str] = None) -> str:
226
+ """Process say command content with variable substitution, converting to tellraw with score components."""
227
+ import re
228
+
229
+ print(f"DEBUG: _process_say_command_with_variables called with content: {repr(content)}, selector: {selector}")
230
+ print(f"DEBUG: Variable scopes available: {variable_scopes}")
231
+
232
+ # Clean up the content - remove quotes if present
233
+ content = content.strip()
234
+ if content.startswith('"') and content.endswith('"'):
235
+ content = content[1:-1] # Remove surrounding quotes
236
+
237
+ # Look for traditional $variable$ syntax
238
+ var_pattern = r'\$([^$]+)\$'
239
+ matches = re.findall(var_pattern, content)
240
+
241
+ if not matches:
242
+ # No variables, return simple tellraw
243
+ return f'tellraw @a [{{"text":"{content}"}}]'
244
+
245
+ # Use re.sub to replace variables with placeholders, then split by placeholders
246
+ # This avoids the issue with re.split including captured groups
247
+ placeholder_content = content
248
+ var_placeholders = []
249
+
250
+ for i, match in enumerate(matches):
251
+ placeholder = f"__VAR_{i}__"
252
+ var_placeholders.append((placeholder, match))
253
+ placeholder_content = placeholder_content.replace(f"${match}$", placeholder, 1)
254
+
255
+ # Split by placeholders to get text parts
256
+ text_parts = placeholder_content
257
+ for placeholder, var_name in var_placeholders:
258
+ text_parts = text_parts.replace(placeholder, f"|{var_name}|")
259
+
260
+ # Now split by the pipe delimiters
261
+ parts = text_parts.split('|')
262
+
263
+ # Build tellraw components
264
+ components = []
265
+
266
+ for i, part in enumerate(parts):
267
+ if i % 2 == 0:
268
+ # Text part
269
+ if part: # Only add non-empty text parts
270
+ components.append(f'{{"text":"{part}"}}')
271
+ else:
272
+ # Variable part
273
+ var_name = part
274
+
275
+ # Check if variable has scope selector
276
+ if '<' in var_name and var_name.endswith('>'):
277
+ # Scoped variable: $variable<selector>$
278
+ var_parts = var_name.split('<', 1)
279
+ base_var = var_parts[0]
280
+ var_selector = var_parts[1][:-1] # Remove trailing >
281
+ components.append(f'{{"score":{{"name":"{var_selector}","objective":"{base_var}"}}}}')
282
+ else:
283
+ # Simple variable: $variable$ - determine selector based on declared scope
284
+ var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]" # Default to global
285
+ if variable_scopes and var_name in variable_scopes:
286
+ declared_scope = variable_scopes[var_name]
287
+ if declared_scope == 'global':
288
+ var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
289
+ else:
290
+ var_selector = declared_scope
291
+ print(f"DEBUG: Variable {var_name} using declared scope {declared_scope} -> selector {var_selector}")
292
+ else:
293
+ print(f"DEBUG: Variable {var_name} has no declared scope, using default global selector")
294
+
295
+ components.append(f'{{"score":{{"name":"{var_selector}","objective":"{var_name}"}}}}')
296
+
297
+ # Join components and create tellraw command
298
+ components_str = ','.join(components)
299
+ return f'tellraw @a [{components_str}]'
300
+
301
+
302
+ 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]:
303
+ """Process a single statement and return Minecraft commands."""
304
+ if variable_scopes is None:
305
+ variable_scopes = {}
306
+
307
+ if build_context is None:
308
+ build_context = BuildContext()
309
+
310
+ commands = []
311
+
312
+ if statement['type'] == 'command':
313
+ command = statement['command']
59
314
 
60
- # Find MDL files
61
- if input_path_obj.is_file():
62
- mdl_files = [input_path_obj] if input_path_obj.suffix == '.mdl' else []
315
+ # Handle say commands specifically
316
+ if command.startswith('say '):
317
+ print(f"DEBUG: Found say command: {repr(command)}")
318
+ # Convert say command to tellraw command
319
+ content = command[4:] # Remove "say " prefix
320
+ print(f"DEBUG: Say command content: {repr(content)}")
321
+ print(f"DEBUG: Raw command from AST: {repr(statement['command'])}")
322
+ # Convert to Minecraft tellraw format
323
+ processed_command = _process_say_command_with_variables(content, selector, variable_scopes)
324
+ print(f"DEBUG: Processed say command: {repr(processed_command)}")
325
+ commands.append(processed_command)
326
+ elif command.startswith('tellraw @a ') or command.startswith('tellraw @ a '):
327
+ # Fix extra space in tellraw commands
328
+ fixed_command = command.replace('tellraw @ a ', 'tellraw @a ')
329
+ commands.append(fixed_command)
63
330
  else:
64
- mdl_files = find_mdl_files(input_path_obj)
331
+ # Process other commands normally
332
+ processed_command = _process_variable_substitutions(command, selector)
333
+ commands.append(processed_command)
334
+
335
+ elif statement['type'] == 'variable_assignment':
336
+ var_name = statement['name']
337
+ value = statement['value']
65
338
 
66
- if not mdl_files:
67
- raise MDLConfigurationError(
68
- message=f"No MDL files found in: {input_path}",
69
- suggestion="Ensure the path contains .mdl files or specify a single .mdl file."
70
- )
339
+ # Determine the correct selector for this variable based on its declared scope
340
+ var_selector = selector # Default to current selector
341
+ if variable_scopes and var_name in variable_scopes:
342
+ declared_scope = variable_scopes[var_name]
343
+ if declared_scope == 'global':
344
+ var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
345
+ else:
346
+ var_selector = declared_scope
347
+ print(f"DEBUG: Variable {var_name} assignment using selector: {var_selector} (declared scope: {variable_scopes.get(var_name, 'none')})")
71
348
 
72
- if verbose:
73
- print_section("Building MDL Project")
74
- print_info(f"Input: {input_path}")
75
- print_info(f"Output: {output_path}")
76
- print_info(f"Found {len(mdl_files)} MDL file(s)")
77
- print_separator()
349
+ # Handle different value types
350
+ if isinstance(value, int):
351
+ commands.append(f"scoreboard players set {var_selector} {var_name} {value}")
352
+ elif isinstance(value, str) and value.startswith('$') and value.endswith('$'):
353
+ # Variable reference
354
+ ref_var = value[1:-1] # Remove $ symbols
355
+ commands.append(f"scoreboard players operation {var_selector} {var_name} = {var_selector} {ref_var}")
356
+ elif hasattr(value, '__class__') and 'BinaryExpression' in str(value.__class__):
357
+ # Handle complex expressions (BinaryExpression, etc.)
358
+ # Convert to proper Minecraft scoreboard commands
359
+ if hasattr(value, 'left') and hasattr(value, 'right') and hasattr(value, 'operator'):
360
+ left = value.left
361
+ right = value.right
362
+ operator = value.operator
363
+
364
+ # Handle different operators
365
+ if operator == 'PLUS':
366
+ if hasattr(left, 'name') and hasattr(right, 'value'):
367
+ # counter = counter + 1
368
+ commands.append(f"scoreboard players add {var_selector} {var_name} {right.value}")
369
+ else:
370
+ # Complex case - use operation
371
+ commands.append(f"# Complex addition: {var_name} = {left} + {right}")
372
+ elif operator == 'MINUS':
373
+ if hasattr(left, 'name') and hasattr(right, 'value'):
374
+ # health = health - 10
375
+ commands.append(f"scoreboard players remove {var_selector} {var_name} {right.value}")
376
+ else:
377
+ # Complex case - use operation
378
+ commands.append(f"# Complex operation: {var_name} = {left} - {right}")
379
+ else:
380
+ # Other operators - use operation
381
+ commands.append(f"# Complex operation: {var_name} = {left} {operator} {right}")
382
+ else:
383
+ commands.append(f"# Complex assignment: {var_name} = {value}")
384
+ else:
385
+ # Handle LiteralExpression and other value types
386
+ try:
387
+ if hasattr(value, 'value'):
388
+ # LiteralExpression case
389
+ num_value = int(value.value)
390
+ commands.append(f"scoreboard players set {var_selector} {var_name} {num_value}")
391
+ else:
392
+ # Direct value case
393
+ num_value = int(value)
394
+ commands.append(f"scoreboard players set {var_selector} {var_name} {num_value}")
395
+ except (ValueError, TypeError):
396
+ # If we can't convert to int, add a placeholder
397
+ commands.append(f"# Assignment: {var_name} = {value}")
398
+
399
+ elif statement['type'] == 'if_statement':
400
+ condition = statement['condition']
401
+ then_body = statement['then_body']
402
+ else_body = statement.get('else_body', [])
78
403
 
79
- # Create output directory
80
- output_path_obj = Path(output_path)
81
- if output_path_obj.exists():
82
- if verbose:
83
- print_warning(f"Output directory already exists: {output_path}")
84
- print_info("Removing existing directory...")
85
- shutil.rmtree(output_path_obj)
404
+ # Convert condition to Minecraft syntax
405
+ minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector)
86
406
 
87
- output_path_obj.mkdir(parents=True, exist_ok=True)
407
+ # Generate unique function names for conditional blocks
408
+ if_func_name = f"{function_name}_if_{statement_index}"
409
+ else_func_name = f"{function_name}_else_{statement_index}"
88
410
 
89
- # Build the datapack using the existing Pack class
90
- pack = Pack("default", "Default pack", pack_format_override or 48)
411
+ # Create conditional function
412
+ if_commands = []
413
+ for i, stmt in enumerate(then_body):
414
+ if_commands.extend(_process_statement(stmt, namespace, if_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
91
415
 
92
- # TODO: Implement proper MDL file processing
93
- # For now, create a simple test function to demonstrate the system works
94
- if verbose:
95
- print_info("Creating test function...")
416
+ # Write conditional function
417
+ if if_commands:
418
+ # Use the output directory parameter
419
+ if output_dir:
420
+ if_dir = output_dir / "data" / namespace / "function"
421
+ else:
422
+ if_dir = Path(f"data/{namespace}/function")
423
+ ensure_dir(str(if_dir))
424
+ with open(if_dir / f"{if_func_name}.mcfunction", 'w', encoding='utf-8') as f:
425
+ f.write('\n'.join(if_commands))
96
426
 
97
- # Add a simple test function to show the system works
98
- test_ns = pack.namespace("test")
99
- test_ns.function("hello", "say Hello from MDL!")
427
+ # Create else function if needed
428
+ if else_body:
429
+ else_commands = []
430
+ for i, stmt in enumerate(else_body):
431
+ else_commands.extend(_process_statement(stmt, namespace, else_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
432
+
433
+ if else_commands:
434
+ with open(if_dir / f"{else_func_name}.mcfunction", 'w', encoding='utf-8') as f:
435
+ f.write('\n'.join(else_commands))
100
436
 
101
- if verbose:
102
- print_success("✓ Created test function")
437
+ # Add the conditional execution command
438
+ if else_body:
439
+ commands.append(f"execute if {minecraft_condition} run function {namespace}:{if_func_name}")
440
+ commands.append(f"execute unless {minecraft_condition} run function {namespace}:{else_func_name}")
441
+ # Add if_end function call for cleanup
442
+ commands.append(f"function {namespace}:{function_name}_if_end_{statement_index}")
443
+ else:
444
+ commands.append(f"execute if {minecraft_condition} run function {namespace}:{if_func_name}")
445
+
446
+ elif statement['type'] == 'while_loop' or statement['type'] == 'while_statement':
447
+ # Handle while loops using recursion
448
+ loop_commands = _process_while_loop_recursion(statement, namespace, function_name, statement_index, is_tag_function, selector, variable_scopes, build_context, output_dir)
449
+ commands.extend(loop_commands)
450
+
451
+ elif statement['type'] == 'function_call':
452
+ func_name = statement['name']
453
+ scope = statement.get('scope')
454
+ func_namespace = statement.get('namespace', namespace) # Use specified namespace or current namespace
103
455
 
104
- # Generate the datapack using the existing build method
105
- if verbose:
106
- print_separator()
107
- print_info("Generating datapack files...")
456
+ if scope:
457
+ # Handle scoped function call
458
+ if scope == 'global':
459
+ # Global scope uses the server armor stand
460
+ selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
461
+ else:
462
+ # Use the specified scope selector
463
+ selector = scope
464
+
465
+ # Generate execute as command
466
+ commands.append(f"execute as {selector} run function {func_namespace}:{func_name}")
467
+ else:
468
+ # Simple function call without scope
469
+ commands.append(f"function {func_namespace}:{func_name}")
470
+
471
+ elif statement['type'] == 'raw_text':
472
+ # Raw Minecraft commands
473
+ raw_commands = statement['commands']
474
+ for cmd in raw_commands:
475
+ processed_cmd = _process_variable_substitutions(cmd, selector)
476
+ commands.append(processed_cmd)
477
+
478
+ return commands
479
+
480
+
481
+ def _generate_function_file(ast: Dict[str, Any], output_dir: Path, namespace: str, verbose: bool = False, build_context: BuildContext = None) -> None:
482
+ """Generate function files from the AST for a specific namespace."""
483
+ if build_context is None:
484
+ build_context = BuildContext()
485
+
486
+ if 'functions' not in ast:
487
+ return
488
+
489
+ # Filter functions by namespace
490
+ namespace_functions = []
491
+ for func in ast['functions']:
492
+ # Check if function belongs to this namespace
493
+ # For now, we'll generate all functions in all namespaces
494
+ # In the future, we could add namespace annotations to functions
495
+ namespace_functions.append(func)
496
+
497
+ if verbose:
498
+ print(f"DEBUG: Processing {len(namespace_functions)} functions for namespace {namespace}")
499
+
500
+ for func in namespace_functions:
501
+ func_name = func['name']
502
+ func_body = func.get('body', [])
108
503
 
109
- try:
110
- pack.build(str(output_path_obj))
111
- if verbose:
112
- print_success("✓ Datapack generated successfully")
113
- except Exception as e:
114
- error_collector.add_error(create_error(
115
- MDLConfigurationError,
116
- f"Failed to generate datapack: {str(e)}",
117
- suggestion="Check the output directory permissions and try again."
118
- ))
504
+ # Collect variable scopes from the AST
505
+ variable_scopes = {}
506
+ if 'variables' in ast:
507
+ for var_decl in ast['variables']:
508
+ var_name = var_decl['name']
509
+ var_scope = var_decl.get('scope')
510
+ if var_scope:
511
+ variable_scopes[var_name] = var_scope
512
+ print(f"DEBUG: Variable {var_name} has scope {var_scope}")
513
+ else:
514
+ print(f"DEBUG: Variable {var_name} has no scope (defaults to @s)")
119
515
 
120
- # Create wrapper if requested
121
- if wrapper:
122
- if verbose:
123
- print_info(f"Creating wrapper: {wrapper}")
124
-
516
+ print(f"DEBUG: Collected variable scopes: {variable_scopes}")
517
+
518
+ # Generate function content
519
+ function_commands = []
520
+ for i, statement in enumerate(func_body):
125
521
  try:
126
- create_wrapper(output_path_obj, wrapper)
127
- if verbose:
128
- print_success(f"✓ Wrapper created: {wrapper}")
522
+ print(f"DEBUG: Processing statement {i} of type {statement.get('type', 'unknown')}: {statement}")
523
+ commands = _process_statement(statement, namespace, func_name, i, False, "@s", variable_scopes, build_context, output_dir)
524
+ function_commands.extend(commands)
525
+ print(f"Generated {len(commands)} commands for statement {i} in function {func_name}: {commands}")
129
526
  except Exception as e:
130
- error_collector.add_error(create_error(
131
- MDLFileError,
132
- f"Failed to create wrapper: {str(e)}",
133
- suggestion="Check file permissions and disk space."
134
- ))
527
+ print(f"Warning: Error processing statement {i} in function {func_name}: {e}")
528
+ import traceback
529
+ traceback.print_exc()
530
+ continue
135
531
 
136
- # Final status
137
- if verbose:
138
- print_separator()
139
- print_success("Build completed successfully!")
140
- print_info(f"Output location: {color.file_path(output_path)}")
141
- if wrapper:
142
- print_info(f"Wrapper file: {color.file_path(wrapper)}")
143
-
144
- # Print any warnings if not ignored
145
- if not ignore_warnings and error_collector.has_warnings():
146
- error_collector.print_errors(verbose=verbose, ignore_warnings=False)
147
-
148
- # Raise if there are any errors
149
- error_collector.raise_if_errors()
532
+ # Write function file
533
+ if function_commands:
534
+ # Only add armor stand setup to main functions that need it
535
+ # Don't add to helper functions or functions in the "other" namespace
536
+ should_add_armor_stand = (namespace != "other" and
537
+ (func_name in ["main", "init", "load"] or
538
+ any(cmd for cmd in function_commands if "scoreboard" in cmd or "tellraw" in cmd)))
539
+
540
+ final_commands = []
541
+ if should_add_armor_stand:
542
+ 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}")
543
+ final_commands.extend(function_commands)
544
+
545
+ if verbose:
546
+ print(f"DEBUG: Final commands for {func_name}: {final_commands}")
547
+
548
+ func_dir = output_dir / "data" / namespace / "function"
549
+ ensure_dir(str(func_dir))
550
+
551
+ with open(func_dir / f"{func_name}.mcfunction", 'w', encoding='utf-8') as f:
552
+ content = '\n'.join(final_commands)
553
+ if verbose:
554
+ print(f"DEBUG: Writing to file {func_name}.mcfunction: {repr(content)}")
555
+ f.write(content)
556
+
557
+ if verbose:
558
+ print(f"Generated function: {namespace}:{func_name}")
559
+ else:
560
+ if verbose:
561
+ print(f"No commands generated for function: {namespace}:{func_name}")
562
+ print(f"Function body: {func_body}")
563
+
564
+
565
+ def _generate_hook_files(ast: Dict[str, Any], output_dir: Path, namespace: str, build_context: BuildContext = None, all_namespaces: List[str] = None) -> None:
566
+ """Generate load and tick tag files."""
567
+ if build_context is None:
568
+ build_context = BuildContext()
569
+
570
+ if all_namespaces is None:
571
+ all_namespaces = [namespace]
572
+
573
+ # Generate load tag
574
+ load_tag_dir = output_dir / "data" / "minecraft" / "tags" / "function"
575
+ ensure_dir(str(load_tag_dir))
576
+
577
+ # Start with load functions for all namespaces
578
+ load_values = []
579
+ for ns in all_namespaces:
580
+ load_values.append(f"{ns}:load")
581
+
582
+ # Add functions specified in on_load hooks
583
+ if 'hooks' in ast:
584
+ for hook in ast['hooks']:
585
+ if hook['hook_type'] == 'load':
586
+ load_values.append(hook['function_name'])
587
+
588
+ # Add pack-specific load function if pack name is available and different from namespace
589
+ if 'pack' in ast and 'name' in ast['pack']:
590
+ pack_name = ast['pack']['name']
591
+ pack_load_function = f"{pack_name}:load"
592
+ # Only add if it's not already in the list (avoids duplicates when pack name == namespace)
593
+ if pack_load_function not in load_values:
594
+ load_values.append(pack_load_function)
595
+
596
+ # Remove duplicates while preserving order
597
+ seen = set()
598
+ unique_load_values = []
599
+ for value in load_values:
600
+ if value not in seen:
601
+ seen.add(value)
602
+ unique_load_values.append(value)
603
+
604
+ load_tag_content = {
605
+ "values": unique_load_values
606
+ }
607
+ write_json(str(load_tag_dir / "load.json"), load_tag_content)
608
+
609
+ # Generate tick tag if there are tick functions
610
+ tick_functions = []
611
+ if 'functions' in ast:
612
+ for func in ast['functions']:
613
+ if func.get('name', '').startswith('tick'):
614
+ tick_functions.append(f"{namespace}:{func['name']}")
615
+
616
+ if tick_functions:
617
+ # Remove duplicates while preserving order
618
+ seen = set()
619
+ unique_tick_functions = []
620
+ for value in tick_functions:
621
+ if value not in seen:
622
+ seen.add(value)
623
+ unique_tick_functions.append(value)
150
624
 
151
- except Exception as e:
152
- if not isinstance(e, (MDLConfigurationError, MDLFileError)):
153
- error_collector.add_error(create_error(
154
- MDLConfigurationError,
155
- f"Unexpected build error: {str(e)}",
156
- suggestion="If this error persists, please report it as a bug."
157
- ))
625
+ tick_tag_content = {
626
+ "values": unique_tick_functions
627
+ }
628
+ write_json(str(load_tag_dir / "tick.json"), tick_tag_content)
629
+
630
+
631
+ def _generate_global_load_function(ast: Dict[str, Any], output_dir: Path, namespace: str, build_context: BuildContext = None) -> None:
632
+ """Generate the global load function."""
633
+ if build_context is None:
634
+ build_context = BuildContext()
635
+
636
+ # Generate scoreboard objectives
637
+ scoreboard_commands = _generate_scoreboard_objectives(ast, output_dir)
638
+
639
+ # Generate load function
640
+ _generate_load_function(scoreboard_commands, output_dir, namespace, ast)
641
+
642
+
643
+ def _generate_tag_files(ast: Dict[str, Any], output_dir: Path, namespace: str) -> None:
644
+ """Generate tag files for the datapack."""
645
+ # This is handled by _generate_hook_files
646
+ pass
647
+
648
+
649
+ def _validate_pack_format(pack_format: int) -> None:
650
+ """Validate the pack format number."""
651
+ if not isinstance(pack_format, int) or pack_format < 1:
652
+ raise ValueError(f"Invalid pack format: {pack_format}. Must be a positive integer.")
653
+
654
+
655
+ 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]:
656
+ """Collect conditional functions from if statements."""
657
+ if variable_scopes is None:
658
+ variable_scopes = {}
659
+
660
+ if build_context is None:
661
+ build_context = BuildContext()
662
+
663
+ conditional_functions = []
664
+
665
+ # Generate function name for this conditional
666
+ if_func_name = f"{function_name}_if_{statement_index}"
667
+ conditional_functions.append(if_func_name)
668
+
669
+ # Process then body
670
+ for i, stmt in enumerate(if_statement['then_body']):
671
+ if stmt['type'] == 'if_statement':
672
+ nested_functions = _collect_conditional_functions(stmt, namespace, if_func_name, i, is_tag_function, selector, variable_scopes, build_context)
673
+ conditional_functions.extend(nested_functions)
674
+
675
+ # Process else body if it exists
676
+ if 'else_body' in if_statement and if_statement['else_body']:
677
+ else_func_name = f"{function_name}_else_{statement_index}"
678
+ conditional_functions.append(else_func_name)
158
679
 
159
- error_collector.print_errors(verbose=verbose, ignore_warnings=ignore_warnings)
160
- error_collector.raise_if_errors()
680
+ for i, stmt in enumerate(if_statement['else_body']):
681
+ if stmt['type'] == 'if_statement':
682
+ nested_functions = _collect_conditional_functions(stmt, namespace, else_func_name, i, is_tag_function, selector, variable_scopes, build_context)
683
+ conditional_functions.extend(nested_functions)
684
+
685
+ return conditional_functions
161
686
 
162
687
 
163
- def create_wrapper(output_dir: Path, wrapper_name: str) -> None:
164
- """Create a zip wrapper around the output directory.
688
+ 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]:
689
+ """Process while loops using recursive function calls."""
690
+ if variable_scopes is None:
691
+ variable_scopes = {}
165
692
 
166
- Args:
167
- output_dir: Directory to wrap
168
- wrapper_name: Name of the zip file
169
- """
170
- wrapper_path = Path(wrapper_name)
693
+ if build_context is None:
694
+ build_context = BuildContext()
171
695
 
172
- # Ensure .zip extension
173
- if not wrapper_path.suffix.lower() == '.zip':
174
- wrapper_path = wrapper_path.with_suffix('.zip')
696
+ condition = while_statement['condition']
697
+ body = while_statement['body']
175
698
 
176
- # Remove existing wrapper if it exists
177
- if wrapper_path.exists():
178
- wrapper_path.unlink()
699
+ # Generate unique function names - they should be the same according to the test
700
+ loop_func_name = f"test_{function_name}_while_{statement_index}"
179
701
 
180
- # Create the zip file
181
- with zipfile.ZipFile(wrapper_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
182
- for file_path in output_dir.rglob('*'):
183
- if file_path.is_file():
184
- # Calculate relative path within the zip
185
- arcname = file_path.relative_to(output_dir)
186
- zipf.write(file_path, arcname)
702
+ # Process loop body
703
+ body_commands = []
704
+ for i, stmt in enumerate(body):
705
+ body_commands.extend(_process_statement(stmt, namespace, loop_func_name, i, is_tag_function, selector, variable_scopes, build_context, output_dir))
706
+
707
+ # Add the recursive call to the loop body
708
+ minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector)
709
+ body_commands.append(f"execute if {minecraft_condition} run function {namespace}:{loop_func_name}")
710
+
711
+ # Write the single loop function
712
+ if body_commands:
713
+ # Use the output directory parameter
714
+ if output_dir:
715
+ func_dir = output_dir / "data" / namespace / "function"
716
+ else:
717
+ func_dir = Path(f"data/{namespace}/function")
718
+ ensure_dir(str(func_dir))
719
+ with open(func_dir / f"{loop_func_name}.mcfunction", 'w', encoding='utf-8') as f:
720
+ f.write('\n'.join(body_commands))
721
+
722
+ # Return the command to start the loop with conditional execution
723
+ return [f"execute if {minecraft_condition} run function {namespace}:{loop_func_name}"]
187
724
 
188
725
 
189
- def build_single_file(mdl_file: Path, output_dir: Path, verbose: bool = False) -> None:
190
- """Build a single MDL file.
726
+ 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]:
727
+ """Process while loops using scheduled functions."""
728
+ if variable_scopes is None:
729
+ variable_scopes = {}
191
730
 
192
- Args:
193
- mdl_file: Path to the MDL file
194
- output_dir: Output directory
195
- verbose: Enable verbose output
196
- """
197
- if verbose:
198
- print_info(f"Building single file: {color.file_path(mdl_file.name)}")
731
+ if build_context is None:
732
+ build_context = BuildContext()
199
733
 
200
- # Create a simple pack for single file builds
201
- pack = Pack("single", "Single file pack", 48)
734
+ condition = while_statement['condition']
735
+ body = while_statement['body']
202
736
 
203
- # TODO: Implement proper MDL file processing
204
- # For now, create a test function
205
- test_ns = pack.namespace("test")
206
- test_ns.function("main", "say Hello from single file!")
737
+ # Generate unique function names
738
+ loop_func_name = f"{function_name}_while_{statement_index}"
739
+ loop_body_func_name = f"{function_name}_while_body_{statement_index}"
207
740
 
208
- pack.build(str(output_dir))
741
+ # Process loop body
742
+ body_commands = []
743
+ for i, stmt in enumerate(body):
744
+ body_commands.extend(_process_statement(stmt, namespace, loop_body_func_name, i, is_tag_function, selector, variable_scopes, build_context))
209
745
 
210
- if verbose:
211
- print_success(f"✓ Built: {color.file_path(mdl_file.name)}")
746
+ # Add the loop continuation command
747
+ minecraft_condition = _convert_condition_to_minecraft_syntax(condition, selector)
748
+ body_commands.append(f"execute {minecraft_condition} run schedule function {namespace}:{loop_body_func_name} 1t")
749
+
750
+ # Write loop body function
751
+ if body_commands:
752
+ # Use the output directory from build context
753
+ if hasattr(build_context, 'output_dir'):
754
+ func_dir = build_context.output_dir / "data" / namespace / "function"
755
+ else:
756
+ func_dir = Path(f"data/{namespace}/function")
757
+ ensure_dir(str(func_dir))
758
+ with open(func_dir / f"{loop_body_func_name}.mcfunction", 'w', encoding='utf-8') as f:
759
+ f.write('\n'.join(body_commands))
760
+
761
+ # Return the command to start the loop
762
+ return [f"schedule function {namespace}:{loop_body_func_name} 1t"]
763
+
212
764
 
765
+ def _create_zip_file(source_dir: Path, zip_path: Path) -> None:
766
+ """Create a zip file from a directory."""
767
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
768
+ for file_path in source_dir.rglob('*'):
769
+ if file_path.is_file():
770
+ arcname = file_path.relative_to(source_dir)
771
+ zipf.write(file_path, arcname)
213
772
 
214
- def build_directory(input_dir: Path, output_dir: Path, verbose: bool = False) -> None:
215
- """Build all MDL files in a directory.
773
+
774
+ def _generate_pack_mcmeta(ast: Dict[str, Any], output_dir: Path) -> None:
775
+ """Generate the pack.mcmeta file."""
776
+ pack_format = ast.get('pack', {}).get('format', 82)
777
+ pack_description = ast.get('pack', {}).get('description', 'MDL Generated Datapack')
216
778
 
217
- Args:
218
- input_dir: Input directory containing MDL files
219
- output_dir: Output directory
220
- verbose: Enable verbose output
221
- """
222
- if verbose:
223
- print_info(f"Building directory: {color.file_path(input_dir.name)}")
779
+ pack_meta = {
780
+ "pack": {
781
+ "pack_format": pack_format,
782
+ "description": pack_description
783
+ }
784
+ }
224
785
 
225
- mdl_files = find_mdl_files(input_dir)
786
+ write_json(str(output_dir / "pack.mcmeta"), pack_meta)
787
+
788
+
789
+ def _ast_to_pack(ast: Dict[str, Any], mdl_files: List[Path]) -> Pack:
790
+ """Convert AST to Pack object."""
791
+ pack_info = ast.get('pack', {})
792
+ if pack_info is None:
793
+ pack_info = {}
794
+ pack_name = pack_info.get('name', 'mdl_pack')
795
+ pack_format = pack_info.get('pack_format', 82) # Use pack_format instead of format
796
+ pack_description = pack_info.get('description', 'MDL Generated Datapack')
226
797
 
227
- if not mdl_files:
228
- raise MDLConfigurationError(
229
- message=f"No MDL files found in directory: {input_dir}",
230
- suggestion="Ensure the directory contains .mdl files."
231
- )
798
+ pack = Pack(pack_name, pack_description, pack_format)
232
799
 
233
- # Create a pack for directory builds
234
- pack = Pack("directory", "Directory pack", 48)
800
+ # Add namespaces and functions
801
+ if 'functions' in ast:
802
+ # Get namespace name from AST or use pack name
803
+ namespace_info = ast.get('namespace', {})
804
+ if namespace_info is None:
805
+ namespace_info = {}
806
+ namespace_name = namespace_info.get('name', pack_name)
807
+ namespace = pack.namespace(namespace_name)
808
+
809
+ for func in ast['functions']:
810
+ function_name = func['name']
811
+ # Create function and add commands if they exist
812
+ function = namespace.function(function_name)
813
+
814
+ # Add commands from function body if they exist
815
+ if 'body' in func:
816
+ for i, statement in enumerate(func['body']):
817
+ try:
818
+ # Use the same processing logic as the build system
819
+ commands = _process_statement(statement, namespace_name, function_name, i, False, "@s", {}, BuildContext())
820
+ function.commands.extend(commands)
821
+ except Exception as e:
822
+ # If processing fails, try to add as simple command
823
+ if statement.get('type') == 'command':
824
+ function.commands.append(statement['command'])
825
+ elif statement.get('type') == 'function_call':
826
+ func_name = statement['name']
827
+ scope = statement.get('scope')
828
+ func_namespace = statement.get('namespace', namespace_name) # Use specified namespace or current namespace
829
+
830
+ if scope:
831
+ # Handle scoped function call
832
+ if scope == 'global':
833
+ # Global scope uses the server armor stand
834
+ selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
835
+ else:
836
+ # Use the specified scope selector
837
+ selector = scope
838
+
839
+ # Generate execute as command
840
+ function.commands.append(f"execute as {selector} run function {func_namespace}:{func_name}")
841
+ else:
842
+ # Simple function call without scope
843
+ function.commands.append(f"function {func_namespace}:{func_name}")
844
+ elif statement.get('type') == 'variable_assignment':
845
+ # Handle variable assignments
846
+ var_name = statement['name']
847
+ value = statement['value']
848
+
849
+ # Determine selector based on variable scope
850
+ var_selector = "@s" # Default
851
+ if 'variables' in ast:
852
+ for var_decl in ast['variables']:
853
+ if var_decl.get('name') == var_name:
854
+ var_scope = var_decl.get('scope')
855
+ if var_scope == 'global':
856
+ var_selector = "@e[type=armor_stand,tag=mdl_server,limit=1]"
857
+ elif var_scope:
858
+ var_selector = var_scope
859
+ break
860
+
861
+ if hasattr(value, 'value'):
862
+ # Simple literal value
863
+ function.commands.append(f"scoreboard players set {var_name} {var_selector} {value.value}")
864
+ else:
865
+ # Complex expression - add a placeholder
866
+ function.commands.append(f"# Variable assignment: {var_name} = {value}")
867
+ else:
868
+ # Add a placeholder for other statement types
869
+ function.commands.append(f"# Statement: {statement.get('type', 'unknown')}")
235
870
 
236
- # TODO: Implement proper MDL file processing
237
- # For now, create a test function for each file
238
- for mdl_file in mdl_files:
239
- if verbose:
240
- print_info(f"Adding: {color.file_path(mdl_file.name)}")
871
+ # Add variables
872
+ if 'variables' in ast:
873
+ for var in ast['variables']:
874
+ # Variables are handled during command processing
875
+ pass
876
+
877
+ # Add hooks
878
+ if 'hooks' in ast:
879
+ for hook in ast['hooks']:
880
+ if hook['hook_type'] == 'load':
881
+ pack.on_load(hook['function_name'])
882
+ elif hook['hook_type'] == 'tick':
883
+ pack.on_tick(hook['function_name'])
884
+
885
+ # Add recipes
886
+ if 'recipes' in ast:
887
+ namespace_info = ast.get('namespace', {})
888
+ if namespace_info is None:
889
+ namespace_info = {}
890
+ namespace_name = namespace_info.get('name', pack_name)
891
+ namespace = pack.namespace(namespace_name)
241
892
 
242
- # Create a namespace for each file
243
- ns_name = f"file_{len(pack.namespaces)}"
244
- test_ns = pack.namespace(ns_name)
245
- test_ns.function("main", f"say Hello from {mdl_file.name}!")
893
+ for recipe in ast['recipes']:
894
+ recipe_name = recipe['name']
895
+ recipe_data = recipe['data']
896
+ # Create recipe object
897
+ from .pack import Recipe
898
+ recipe_obj = Recipe(recipe_name, recipe_data)
899
+ namespace.recipes[recipe_name] = recipe_obj
246
900
 
247
- pack.build(str(output_dir))
901
+ # Add advancements
902
+ if 'advancements' in ast:
903
+ namespace_info = ast.get('namespace', {})
904
+ if namespace_info is None:
905
+ namespace_info = {}
906
+ namespace_name = namespace_info.get('name', pack_name)
907
+ namespace = pack.namespace(namespace_name)
908
+
909
+ for advancement in ast['advancements']:
910
+ advancement_name = advancement['name']
911
+ advancement_data = advancement['data']
912
+ # Create advancement object
913
+ from .pack import Advancement
914
+ advancement_obj = Advancement(advancement_name, advancement_data)
915
+ namespace.advancements[advancement_name] = advancement_obj
248
916
 
249
- if verbose:
250
- print_success(f"✓ Built {len(mdl_files)} file(s) from {color.file_path(input_dir.name)}")
917
+ # Add loot tables
918
+ if 'loot_tables' in ast:
919
+ namespace_info = ast.get('namespace', {})
920
+ if namespace_info is None:
921
+ namespace_info = {}
922
+ namespace_name = namespace_info.get('name', pack_name)
923
+ namespace = pack.namespace(namespace_name)
924
+
925
+ for loot_table in ast['loot_tables']:
926
+ loot_table_name = loot_table['name']
927
+ loot_table_data = loot_table['data']
928
+ # Create loot table object
929
+ from .pack import LootTable
930
+ loot_table_obj = LootTable(loot_table_name, loot_table_data)
931
+ namespace.loot_tables[loot_table_name] = loot_table_obj
932
+
933
+ return pack
934
+
935
+
936
+ 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:
937
+ """Build MDL files into a Minecraft datapack."""
938
+ error_collector = MDLErrorCollector()
939
+
940
+ try:
941
+ input_dir = Path(input_path)
942
+ output_dir = Path(output_path)
943
+
944
+ # Validate input directory exists
945
+ if not input_dir.exists():
946
+ error_collector.add_error(create_error(
947
+ MDLFileError,
948
+ f"Input path does not exist: {input_path}",
949
+ file_path=input_path,
950
+ suggestion="Check the path and ensure the file or directory exists."
951
+ ))
952
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
953
+ error_collector.raise_if_errors()
954
+ return
955
+
956
+ # Find MDL files
957
+ mdl_files = _find_mdl_files(input_dir)
958
+
959
+ if not mdl_files:
960
+ error_collector.add_error(create_error(
961
+ MDLFileError,
962
+ f"No .mdl files found in {input_path}",
963
+ file_path=input_path,
964
+ suggestion="Ensure the directory contains .mdl files or specify a single .mdl file."
965
+ ))
966
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
967
+ error_collector.raise_if_errors()
968
+ return
969
+
970
+ if verbose:
971
+ print_section("Building MDL Project")
972
+ print_info(f"Found {len(mdl_files)} MDL file(s):")
973
+ for file in mdl_files:
974
+ print_info(f" - {color.file_path(str(file))}")
975
+ print_separator()
976
+
977
+ # Merge and parse MDL files
978
+ ast = _merge_mdl_files(mdl_files, verbose, error_collector)
979
+
980
+ if ast is None:
981
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
982
+ error_collector.raise_if_errors()
983
+ return
984
+
985
+ # Override pack format if specified
986
+ if pack_format_override is not None:
987
+ _validate_pack_format(pack_format_override)
988
+ if 'pack' not in ast:
989
+ ast['pack'] = {}
990
+ ast['pack']['format'] = pack_format_override
991
+
992
+ # Create output directory
993
+ ensure_dir(str(output_dir))
994
+
995
+ # Generate pack.mcmeta
996
+ _generate_pack_mcmeta(ast, output_dir)
997
+
998
+ # Handle multiple namespaces from multiple files
999
+ namespaces = []
1000
+
1001
+ # Get namespaces from AST
1002
+ if 'namespaces' in ast:
1003
+ for ns in ast['namespaces']:
1004
+ if 'name' in ns:
1005
+ namespaces.append(_slugify(ns['name']))
1006
+
1007
+ # If no explicit namespaces, use the pack name as default
1008
+ if not namespaces:
1009
+ default_namespace = ast.get('pack', {}).get('name', 'mdl_pack')
1010
+ namespaces.append(_slugify(default_namespace))
1011
+
1012
+ # Debug: Show what namespaces we're using
1013
+ if verbose:
1014
+ print_info(f"AST namespaces: {ast.get('namespaces', [])}")
1015
+ print_info(f"AST pack: {ast.get('pack', {})}")
1016
+ print_info(f"Using namespaces: {namespaces}")
1017
+
1018
+ # Generate functions for each namespace
1019
+ build_context = BuildContext()
1020
+ for namespace in namespaces:
1021
+ if verbose:
1022
+ print_info(f"Processing namespace: {color.highlight(namespace)}")
1023
+ _generate_function_file(ast, output_dir, namespace, verbose, build_context)
1024
+
1025
+ # Generate hook files (load/tick tags) - include all namespaces
1026
+ primary_namespace = namespaces[0] if namespaces else 'mdl_pack'
1027
+ _generate_hook_files(ast, output_dir, primary_namespace, build_context, all_namespaces=namespaces)
1028
+
1029
+ # Generate load functions for all namespaces
1030
+ for namespace in namespaces:
1031
+ _generate_global_load_function(ast, output_dir, namespace, build_context)
1032
+
1033
+ # Create zip file (always create one, use wrapper name if specified)
1034
+ zip_name = wrapper if wrapper else output_dir.name
1035
+ # When using wrapper, create zip in output directory; otherwise in parent directory
1036
+ if wrapper:
1037
+ zip_path = output_dir / f"{zip_name}.zip"
1038
+ else:
1039
+ zip_path = output_dir.parent / f"{zip_name}.zip"
1040
+ _create_zip_file(output_dir, zip_path)
1041
+ if verbose:
1042
+ print_info(f"Created zip file: {color.file_path(str(zip_path))}")
1043
+
1044
+ print_success(f"Successfully built datapack: {color.file_path(output_path)}")
1045
+ if verbose:
1046
+ print_info(f"Output directory: {color.file_path(str(output_dir))}")
1047
+ print_info(f"Namespace: {color.highlight(namespace)}")
1048
+
1049
+ except MDLLexerError as e:
1050
+ error_collector.add_error(e)
1051
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1052
+ error_collector.raise_if_errors()
1053
+ except MDLParserError as e:
1054
+ error_collector.add_error(e)
1055
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1056
+ error_collector.raise_if_errors()
1057
+ except MDLSyntaxError as e:
1058
+ error_collector.add_error(e)
1059
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1060
+ error_collector.raise_if_errors()
1061
+ except MDLBuildError as e:
1062
+ error_collector.add_error(e)
1063
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1064
+ error_collector.raise_if_errors()
1065
+ except MDLFileError as e:
1066
+ error_collector.add_error(e)
1067
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1068
+ error_collector.raise_if_errors()
1069
+ except MDLCompilationError as e:
1070
+ error_collector.add_error(e)
1071
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1072
+ error_collector.raise_if_errors()
1073
+ except Exception as e:
1074
+ error_collector.add_error(create_error(
1075
+ MDLBuildError,
1076
+ f"Unexpected error during build: {str(e)}",
1077
+ suggestion="Check the input files and try again. If the problem persists, report this as a bug."
1078
+ ))
1079
+ error_collector.print_errors(verbose=True, ignore_warnings=ignore_warnings)
1080
+ error_collector.raise_if_errors()