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.
- minecraft_datapack_language/__init__.py +23 -2
- minecraft_datapack_language/_version.py +2 -2
- minecraft_datapack_language/ast_nodes.py +87 -59
- minecraft_datapack_language/cli.py +276 -139
- minecraft_datapack_language/mdl_compiler.py +470 -0
- minecraft_datapack_language/mdl_errors.py +14 -0
- minecraft_datapack_language/mdl_lexer.py +624 -0
- minecraft_datapack_language/mdl_parser.py +573 -0
- minecraft_datapack_language-15.4.30.dist-info/METADATA +266 -0
- minecraft_datapack_language-15.4.30.dist-info/RECORD +17 -0
- minecraft_datapack_language/cli_build.py +0 -1292
- minecraft_datapack_language/cli_check.py +0 -155
- minecraft_datapack_language/cli_colors.py +0 -264
- minecraft_datapack_language/cli_help.py +0 -508
- minecraft_datapack_language/cli_new.py +0 -300
- minecraft_datapack_language/cli_utils.py +0 -276
- minecraft_datapack_language/expression_processor.py +0 -352
- minecraft_datapack_language/linter.py +0 -409
- minecraft_datapack_language/mdl_lexer_js.py +0 -754
- minecraft_datapack_language/mdl_parser_js.py +0 -1049
- minecraft_datapack_language/pack.py +0 -758
- minecraft_datapack_language-15.4.28.dist-info/METADATA +0 -1274
- minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/WHEEL +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/entry_points.txt +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.dist-info}/licenses/LICENSE +0 -0
- {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.30.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()
|