minecraft-datapack-language 15.4.28__py3-none-any.whl → 15.4.30__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 +23 -2
  2. minecraft_datapack_language/_version.py +2 -2
  3. minecraft_datapack_language/ast_nodes.py +87 -59
  4. minecraft_datapack_language/cli.py +276 -139
  5. minecraft_datapack_language/mdl_compiler.py +470 -0
  6. minecraft_datapack_language/mdl_errors.py +14 -0
  7. minecraft_datapack_language/mdl_lexer.py +624 -0
  8. minecraft_datapack_language/mdl_parser.py +573 -0
  9. minecraft_datapack_language-15.4.30.dist-info/METADATA +266 -0
  10. minecraft_datapack_language-15.4.30.dist-info/RECORD +17 -0
  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.28.dist-info/METADATA +0 -1274
  23. minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
  24. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/WHEEL +0 -0
  25. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/entry_points.txt +0 -0
  26. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/licenses/LICENSE +0 -0
  27. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,470 @@
1
+ """
2
+ MDL Compiler - Converts MDL AST into complete Minecraft datapack
3
+ Simplified version that focuses on generating actual statements for testing
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Dict, List, Any, Optional
11
+ from .ast_nodes import (
12
+ Program, PackDeclaration, NamespaceDeclaration, TagDeclaration,
13
+ VariableDeclaration, VariableAssignment, VariableSubstitution, FunctionDeclaration,
14
+ FunctionCall, IfStatement, WhileLoop, HookDeclaration, RawBlock,
15
+ SayCommand, BinaryExpression, LiteralExpression, ParenthesizedExpression
16
+ )
17
+ from .dir_map import get_dir_map, DirMap
18
+ from .mdl_errors import MDLCompilerError
19
+
20
+
21
+ class MDLCompiler:
22
+ """
23
+ Simplified compiler for the MDL language that generates actual statements.
24
+ """
25
+
26
+ def __init__(self, output_dir: str = "dist"):
27
+ self.output_dir = Path(output_dir)
28
+ self.dir_map: Optional[DirMap] = None
29
+ self.current_namespace = "mdl"
30
+ self.variables: Dict[str, str] = {} # name -> objective mapping
31
+
32
+ def compile(self, ast: Program, source_dir: str = None) -> str:
33
+ """Compile MDL AST into a complete Minecraft datapack."""
34
+ try:
35
+ # Use source_dir as output directory if provided
36
+ if source_dir:
37
+ output_dir = Path(source_dir)
38
+ else:
39
+ output_dir = self.output_dir
40
+
41
+ # Clean output directory
42
+ if output_dir.exists():
43
+ shutil.rmtree(output_dir)
44
+ output_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ # Temporarily set output directory
47
+ original_output_dir = self.output_dir
48
+ self.output_dir = output_dir
49
+
50
+ # Set up directory mapping based on pack format
51
+ pack_format = ast.pack.pack_format if ast.pack else 15
52
+ self.dir_map = get_dir_map(pack_format)
53
+
54
+ # Create pack.mcmeta
55
+ self._create_pack_mcmeta(ast.pack)
56
+
57
+ # Create data directory structure
58
+ data_dir = self.output_dir / "data"
59
+ data_dir.mkdir(parents=True, exist_ok=True)
60
+
61
+ # Set namespace
62
+ if ast.namespace:
63
+ self.current_namespace = ast.namespace.name
64
+
65
+ # Create namespace directory
66
+ namespace_dir = data_dir / self.current_namespace
67
+ namespace_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Compile all components
70
+ self._compile_variables(ast.variables, namespace_dir)
71
+ self._compile_functions(ast.functions, namespace_dir)
72
+ self._compile_hooks(ast.hooks, namespace_dir)
73
+ self._compile_statements(ast.statements, namespace_dir)
74
+ self._compile_tags(ast.tags, source_dir)
75
+
76
+ # Create load and tick functions for hooks
77
+ self._create_hook_functions(ast.hooks, namespace_dir)
78
+
79
+ # Return the output directory path
80
+ result = str(self.output_dir)
81
+
82
+ # Restore original output directory
83
+ self.output_dir = original_output_dir
84
+
85
+ return result
86
+
87
+ except Exception as e:
88
+ # Restore original output directory on error
89
+ if 'original_output_dir' in locals():
90
+ self.output_dir = original_output_dir
91
+
92
+ if isinstance(e, MDLCompilerError):
93
+ raise e
94
+ else:
95
+ raise MDLCompilerError(f"Compilation failed: {str(e)}", "Check the AST structure")
96
+
97
+ def _create_pack_mcmeta(self, pack: Optional[PackDeclaration]):
98
+ """Create pack.mcmeta file."""
99
+ if not pack:
100
+ pack_data = {
101
+ "pack": {
102
+ "pack_format": 15,
103
+ "description": "MDL Generated Datapack"
104
+ }
105
+ }
106
+ else:
107
+ pack_data = {
108
+ "pack": {
109
+ "pack_format": pack.pack_format,
110
+ "description": pack.description
111
+ }
112
+ }
113
+
114
+ pack_mcmeta_path = self.output_dir / "pack.mcmeta"
115
+ with open(pack_mcmeta_path, 'w') as f:
116
+ json.dump(pack_data, f, indent=2)
117
+
118
+ def _compile_variables(self, variables: List[VariableDeclaration], namespace_dir: Path):
119
+ """Compile variable declarations into scoreboard objectives."""
120
+ for var in variables:
121
+ objective_name = var.name
122
+ self.variables[var.name] = objective_name
123
+ print(f"Variable: {var.name} -> scoreboard objective '{objective_name}'")
124
+
125
+ def _compile_functions(self, functions: List[FunctionDeclaration], namespace_dir: Path):
126
+ """Compile function declarations into .mcfunction files."""
127
+ if self.dir_map:
128
+ functions_dir = namespace_dir / self.dir_map.function
129
+ else:
130
+ functions_dir = namespace_dir / "functions"
131
+ functions_dir.mkdir(parents=True, exist_ok=True)
132
+
133
+ for func in functions:
134
+ func_file = functions_dir / f"{func.name}.mcfunction"
135
+ content = self._generate_function_content(func)
136
+
137
+ with open(func_file, 'w') as f:
138
+ f.write(content)
139
+
140
+ print(f"Function: {func.namespace}:{func.name} -> {func_file}")
141
+
142
+ def _generate_function_content(self, func: FunctionDeclaration) -> str:
143
+ """Generate the content of a .mcfunction file."""
144
+ lines = []
145
+ lines.append(f"# Function: {func.namespace}:{func.name}")
146
+ if func.scope:
147
+ lines.append(f"# Scope: {func.scope}")
148
+ lines.append("")
149
+
150
+ # Generate commands from function body
151
+ for statement in func.body:
152
+ cmd = self._statement_to_command(statement)
153
+ if cmd:
154
+ lines.append(cmd)
155
+
156
+ return "\n".join(lines)
157
+
158
+ def _compile_hooks(self, hooks: List[HookDeclaration], namespace_dir: Path):
159
+ """Compile hook declarations."""
160
+ for hook in hooks:
161
+ print(f"Hook: {hook.hook_type} -> {hook.namespace}:{hook.name}")
162
+
163
+ def _compile_statements(self, statements: List[Any], namespace_dir: Path):
164
+ """Compile top-level statements."""
165
+ for statement in statements:
166
+ if isinstance(statement, FunctionCall):
167
+ print(f"Top-level exec: {statement.namespace}:{statement.name}")
168
+ elif isinstance(statement, RawBlock):
169
+ print(f"Top-level raw block: {len(statement.content)} characters")
170
+
171
+ def _compile_tags(self, tags: List[TagDeclaration], source_dir: str):
172
+ """Compile tag declarations and copy referenced JSON files."""
173
+ source_path = Path(source_dir) if source_dir else None
174
+
175
+ for tag in tags:
176
+ if tag.tag_type == "recipe":
177
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
178
+ elif tag.tag_type == "loot_table":
179
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
180
+ elif tag.tag_type == "advancement":
181
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
182
+ elif tag.tag_type == "item_modifier":
183
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
184
+ elif tag.tag_type == "predicate":
185
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
186
+ elif tag.tag_type == "structure":
187
+ tag_dir = self.output_dir / "data" / "minecraft" / self.dir_map.tags_item
188
+ else:
189
+ continue
190
+
191
+ tag_dir.mkdir(parents=True, exist_ok=True)
192
+ tag_file = tag_dir / f"{tag.name}.json"
193
+
194
+ if source_path:
195
+ source_json = source_path / tag.file_path
196
+ if source_json.exists():
197
+ shutil.copy2(source_json, tag_file)
198
+ print(f"Tag {tag.tag_type}: {tag.name} -> {tag_file}")
199
+ else:
200
+ tag_data = {"values": [f"{self.current_namespace}:{tag.name}"]}
201
+ with open(tag_file, 'w') as f:
202
+ json.dump(tag_data, f, indent=2)
203
+ print(f"Tag {tag.tag_type}: {tag.name} -> {tag_file} (placeholder)")
204
+ else:
205
+ tag_data = {"values": [f"{self.current_namespace}:{tag.name}"]}
206
+ with open(tag_file, 'w') as f:
207
+ json.dump(tag_data, f, indent=2)
208
+ print(f"Tag {tag.tag_type}: {tag.name} -> {tag_file} (placeholder)")
209
+
210
+ def _create_hook_functions(self, hooks: List[HookDeclaration], namespace_dir: Path):
211
+ """Create load.mcfunction and tick.mcfunction for hooks."""
212
+ if self.dir_map:
213
+ functions_dir = namespace_dir / self.dir_map.function
214
+ else:
215
+ functions_dir = namespace_dir / "functions"
216
+
217
+ # Create load function
218
+ load_content = self._generate_load_function(hooks)
219
+ load_file = functions_dir / "load.mcfunction"
220
+ with open(load_file, 'w') as f:
221
+ f.write(load_content)
222
+
223
+ # Create tick function if needed
224
+ tick_hooks = [h for h in hooks if h.hook_type == "on_tick"]
225
+ if tick_hooks:
226
+ tick_content = self._generate_tick_function(tick_hooks)
227
+ tick_file = functions_dir / "tick.mcfunction"
228
+ with open(tick_file, 'w') as f:
229
+ f.write(tick_content)
230
+
231
+ def _generate_load_function(self, hooks: List[HookDeclaration]) -> str:
232
+ """Generate the content of load.mcfunction."""
233
+ lines = [
234
+ "# Load function - runs when datapack loads",
235
+ "# Generated by MDL Compiler",
236
+ ""
237
+ ]
238
+
239
+ # Add scoreboard objective creation for variables
240
+ for var_name, objective in self.variables.items():
241
+ lines.append(f"scoreboard objectives add {objective} dummy \"{var_name}\"")
242
+
243
+ lines.append("")
244
+
245
+ # Add on_load hook calls
246
+ load_hooks = [h for h in hooks if h.hook_type == "on_load"]
247
+ for hook in load_hooks:
248
+ if hook.scope:
249
+ lines.append(f"execute as {hook.scope.strip('<>')} run function {hook.namespace}:{hook.name}")
250
+ else:
251
+ lines.append(f"function {hook.namespace}:{hook.name}")
252
+
253
+ return "\n".join(lines)
254
+
255
+ def _generate_tick_function(self, tick_hooks: List[HookDeclaration]) -> str:
256
+ """Generate the content of tick.mcfunction."""
257
+ lines = [
258
+ "# Tick function - runs every tick",
259
+ "# Generated by MDL Compiler",
260
+ ""
261
+ ]
262
+
263
+ # Add on_tick hook calls
264
+ for hook in tick_hooks:
265
+ if hook.scope:
266
+ scope = hook.scope.strip("<>")
267
+ lines.append(f"execute as {scope} run function {hook.namespace}:{hook.name}")
268
+ else:
269
+ lines.append(f"function {hook.namespace}:{hook.name}")
270
+
271
+ return "\n".join(lines)
272
+
273
+ def _statement_to_command(self, statement: Any) -> Optional[str]:
274
+ """Convert an AST statement to a Minecraft command."""
275
+ if isinstance(statement, VariableAssignment):
276
+ return self._variable_assignment_to_command(statement)
277
+ elif isinstance(statement, SayCommand):
278
+ return self._say_command_to_command(statement)
279
+ elif isinstance(statement, RawBlock):
280
+ return statement.content
281
+ elif isinstance(statement, IfStatement):
282
+ return self._if_statement_to_command(statement)
283
+ elif isinstance(statement, WhileLoop):
284
+ return self._while_loop_to_command(statement)
285
+ elif isinstance(statement, FunctionCall):
286
+ return self._function_call_to_command(statement)
287
+ else:
288
+ return None
289
+
290
+ def _variable_assignment_to_command(self, assignment: VariableAssignment) -> str:
291
+ """Convert variable assignment to scoreboard command."""
292
+ objective = self.variables.get(assignment.name, assignment.name)
293
+ value = self._expression_to_value(assignment.value)
294
+ scope = assignment.scope.strip("<>")
295
+ return f"scoreboard players set {scope} {objective} {value}"
296
+
297
+ def _say_command_to_command(self, say: SayCommand) -> str:
298
+ """Convert say command to tellraw command with JSON formatting."""
299
+ if not say.variables:
300
+ return f'tellraw @a {{"text":"{say.message}"}}'
301
+ else:
302
+ return self._build_tellraw_json(say.message, say.variables)
303
+
304
+ def _build_tellraw_json(self, message: str, variables: List[VariableSubstitution]) -> str:
305
+ """Build complex tellraw JSON with variable substitutions."""
306
+ parts = []
307
+ current_pos = 0
308
+
309
+ for var in variables:
310
+ var_pattern = f"${var.name}{var.scope}$"
311
+ var_pos = message.find(var_pattern, current_pos)
312
+
313
+ if var_pos != -1:
314
+ if var_pos > current_pos:
315
+ text_before = message[current_pos:var_pos]
316
+ parts.append(f'{{"text":"{text_before}"}}')
317
+
318
+ objective = self.variables.get(var.name, var.name)
319
+ scope = var.scope.strip("<>")
320
+ parts.append(f'{{"score":{{"name":"{scope}","objective":"{objective}"}}}}')
321
+
322
+ current_pos = var_pos + len(var_pattern)
323
+
324
+ if current_pos < len(message):
325
+ text_after = message[current_pos:]
326
+ parts.append(text_after)
327
+
328
+ if len(parts) == 1:
329
+ if isinstance(parts[0], str) and not parts[0].startswith('{"'):
330
+ return f'tellraw @a {{"text":"{parts[0]}"}}'
331
+ return f'tellraw @a {parts[0]}'
332
+ else:
333
+ first_part = parts[0]
334
+ remaining_parts = parts[1:]
335
+ if remaining_parts:
336
+ import json
337
+ first_data = json.loads(first_part)
338
+ extra_parts = []
339
+ for part in remaining_parts:
340
+ if isinstance(part, str) and not part.startswith('{"'):
341
+ extra_parts.append(f'"{part}"')
342
+ else:
343
+ extra_parts.append(part)
344
+
345
+ extra_json = ",".join(extra_parts)
346
+ return f'tellraw @a {{"text":"{first_data["text"]}","extra":[{extra_json}]}}'
347
+ else:
348
+ return f'tellraw @a {first_part}'
349
+
350
+ def _if_statement_to_command(self, if_stmt: IfStatement) -> str:
351
+ """Convert if statement to comment and include actual statements."""
352
+ condition = self._expression_to_condition(if_stmt.condition)
353
+ lines = [f"# if {condition}"]
354
+
355
+ # Include the actual statements from the if body for visibility
356
+ for stmt in if_stmt.then_body:
357
+ if isinstance(stmt, VariableAssignment):
358
+ # Include variable assignments directly
359
+ cmd = self._variable_assignment_to_command(stmt)
360
+ lines.append(cmd)
361
+ elif isinstance(stmt, SayCommand):
362
+ # Include say commands directly
363
+ cmd = self._say_command_to_command(stmt)
364
+ lines.append(cmd)
365
+ elif isinstance(stmt, RawBlock):
366
+ # Include raw blocks directly
367
+ lines.append(stmt.content)
368
+ elif isinstance(stmt, IfStatement):
369
+ # Recursively handle nested if statements
370
+ cmd = self._if_statement_to_command(stmt)
371
+ lines.append(cmd)
372
+ elif isinstance(stmt, WhileLoop):
373
+ # Handle while loops
374
+ cmd = self._while_loop_to_command(stmt)
375
+ lines.append(cmd)
376
+ elif isinstance(stmt, FunctionCall):
377
+ # Handle function calls
378
+ cmd = self._function_call_to_command(stmt)
379
+ lines.append(cmd)
380
+
381
+ # Handle else body if it exists
382
+ if if_stmt.else_body:
383
+ lines.append("")
384
+ lines.append("# else:")
385
+ for stmt in if_stmt.else_body:
386
+ if isinstance(stmt, VariableAssignment):
387
+ cmd = self._variable_assignment_to_command(stmt)
388
+ lines.append(cmd)
389
+ elif isinstance(stmt, SayCommand):
390
+ cmd = self._say_command_to_command(stmt)
391
+ lines.append(cmd)
392
+ elif isinstance(stmt, RawBlock):
393
+ lines.append(stmt.content)
394
+ elif isinstance(stmt, IfStatement):
395
+ cmd = self._if_statement_to_command(stmt)
396
+ lines.append(cmd)
397
+ elif isinstance(stmt, WhileLoop):
398
+ cmd = self._while_loop_to_command(stmt)
399
+ lines.append(cmd)
400
+ elif isinstance(stmt, FunctionCall):
401
+ cmd = self._function_call_to_command(stmt)
402
+ lines.append(cmd)
403
+
404
+ return "\n".join(lines)
405
+
406
+ def _while_loop_to_command(self, while_loop: WhileLoop) -> str:
407
+ """Convert while loop to comment and include actual statements."""
408
+ condition = self._expression_to_condition(while_loop.condition)
409
+ lines = [f"# while {condition}"]
410
+
411
+ # Include the actual statements from the while body for visibility
412
+ for stmt in while_loop.body:
413
+ if isinstance(stmt, VariableAssignment):
414
+ # Include variable assignments directly
415
+ cmd = self._variable_assignment_to_command(stmt)
416
+ lines.append(cmd)
417
+ elif isinstance(stmt, SayCommand):
418
+ # Include say commands directly
419
+ cmd = self._say_command_to_command(stmt)
420
+ lines.append(cmd)
421
+ elif isinstance(stmt, RawBlock):
422
+ # Include raw blocks directly
423
+ lines.append(stmt.content)
424
+ elif isinstance(stmt, IfStatement):
425
+ # Recursively handle nested if statements
426
+ cmd = self._if_statement_to_command(stmt)
427
+ lines.append(cmd)
428
+ elif isinstance(stmt, WhileLoop):
429
+ # Handle nested while loops
430
+ cmd = self._while_loop_to_command(stmt)
431
+ lines.append(cmd)
432
+ elif isinstance(stmt, FunctionCall):
433
+ # Handle function calls
434
+ cmd = self._function_call_to_command(stmt)
435
+ lines.append(cmd)
436
+
437
+ return "\n".join(lines)
438
+
439
+ def _function_call_to_command(self, func_call: FunctionCall) -> str:
440
+ """Convert function call to execute command."""
441
+ if func_call.scope:
442
+ return f"execute as {func_call.scope.strip('<>')} run function {func_call.namespace}:{func_call.name}"
443
+ else:
444
+ return f"function {func_call.namespace}:{func_call.name}"
445
+
446
+ def _expression_to_value(self, expression: Any) -> str:
447
+ """Convert expression to a value string."""
448
+ if isinstance(expression, LiteralExpression):
449
+ return str(expression.value)
450
+ elif isinstance(expression, VariableSubstitution):
451
+ objective = self.variables.get(expression.name, expression.name)
452
+ scope = expression.scope.strip("<>")
453
+ return f"score {scope} {objective}"
454
+ elif isinstance(expression, BinaryExpression):
455
+ left = self._expression_to_value(expression.left)
456
+ right = self._expression_to_value(expression.right)
457
+ return f"{left} {expression.operator} {right}"
458
+ elif isinstance(expression, ParenthesizedExpression):
459
+ return f"({self._expression_to_value(expression.expression)})"
460
+ else:
461
+ return str(expression)
462
+
463
+ def _expression_to_condition(self, expression: Any) -> str:
464
+ """Convert expression to a condition string."""
465
+ if isinstance(expression, BinaryExpression):
466
+ left = self._expression_to_value(expression.left)
467
+ right = self._expression_to_value(expression.right)
468
+ return f"{left} {expression.operator} {right}"
469
+ else:
470
+ return self._expression_to_value(expression)
@@ -185,6 +185,19 @@ class MDLCompilationError(MDLError):
185
185
  return f"Compilation Error: {super().__str__()}"
186
186
 
187
187
 
188
+ @dataclass
189
+ class MDLCompilerError(MDLError):
190
+ """Error during compilation process."""
191
+ error_type: str = "compiler_error"
192
+
193
+ def __str__(self) -> str:
194
+ try:
195
+ from .cli_colors import color
196
+ return f"{color.error_type('Compiler Error:')} {super().__str__()}"
197
+ except ImportError:
198
+ return f"Compiler Error: {super().__str__()}"
199
+
200
+
188
201
  @dataclass
189
202
  class MDLFileError(MDLError):
190
203
  """Error related to file operations."""
@@ -302,6 +315,7 @@ def create_error(error_type: str, message: str, file_path: Optional[str] = None,
302
315
  "validation": MDLValidationError,
303
316
  "build": MDLBuildError,
304
317
  "compilation": MDLCompilationError,
318
+ "compiler": MDLCompilerError,
305
319
  "file": MDLFileError,
306
320
  "configuration": MDLConfigurationError
307
321
  }