minecraft-datapack-language 15.4.27__py3-none-any.whl → 15.4.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minecraft_datapack_language/__init__.py +17 -2
- minecraft_datapack_language/_version.py +2 -2
- minecraft_datapack_language/ast_nodes.py +87 -59
- 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.29.dist-info/METADATA +266 -0
- minecraft_datapack_language-15.4.29.dist-info/RECORD +16 -0
- minecraft_datapack_language/cli.py +0 -159
- 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.27.dist-info/METADATA +0 -1274
- minecraft_datapack_language-15.4.27.dist-info/RECORD +0 -25
- {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/WHEEL +0 -0
- {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/entry_points.txt +0 -0
- {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/licenses/LICENSE +0 -0
- {minecraft_datapack_language-15.4.27.dist-info → minecraft_datapack_language-15.4.29.dist-info}/top_level.txt +0 -0
@@ -1,758 +0,0 @@
|
|
1
|
-
|
2
|
-
from __future__ import annotations
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from typing import List, Dict, Optional, Union
|
5
|
-
import os, json
|
6
|
-
from .dir_map import get_dir_map, DirMap
|
7
|
-
from .utils import ensure_dir, write_json, write_text
|
8
|
-
|
9
|
-
@dataclass
|
10
|
-
class Function:
|
11
|
-
name: str # name within namespace e.g. "tick" or "folder/thing"
|
12
|
-
commands: List[str] = field(default_factory=list)
|
13
|
-
|
14
|
-
@dataclass
|
15
|
-
class TagFile:
|
16
|
-
path: str # e.g. "minecraft:tick" or "my_ns:foo"
|
17
|
-
values: List[str] = field(default_factory=list)
|
18
|
-
replace: bool = False
|
19
|
-
|
20
|
-
@dataclass
|
21
|
-
class Recipe:
|
22
|
-
name: str
|
23
|
-
data: dict
|
24
|
-
|
25
|
-
@dataclass
|
26
|
-
class Advancement:
|
27
|
-
name: str
|
28
|
-
data: dict
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class LootTable:
|
32
|
-
name: str
|
33
|
-
data: dict
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class Predicate:
|
37
|
-
name: str
|
38
|
-
data: dict
|
39
|
-
|
40
|
-
@dataclass
|
41
|
-
class ItemModifier:
|
42
|
-
name: str
|
43
|
-
data: dict
|
44
|
-
|
45
|
-
@dataclass
|
46
|
-
class Structure:
|
47
|
-
name: str
|
48
|
-
data: dict # we treat this as JSON until external tools produce .nbt
|
49
|
-
|
50
|
-
@dataclass
|
51
|
-
class Namespace:
|
52
|
-
name: str
|
53
|
-
functions: Dict[str, Function] = field(default_factory=dict)
|
54
|
-
recipes: Dict[str, Recipe] = field(default_factory=dict)
|
55
|
-
advancements: Dict[str, Advancement] = field(default_factory=dict)
|
56
|
-
loot_tables: Dict[str, LootTable] = field(default_factory=dict)
|
57
|
-
predicates: Dict[str, Predicate] = field(default_factory=dict)
|
58
|
-
item_modifiers: Dict[str, ItemModifier] = field(default_factory=dict)
|
59
|
-
structures: Dict[str, Structure] = field(default_factory=dict)
|
60
|
-
|
61
|
-
def function(self, name: str, *commands: str) -> Function:
|
62
|
-
fn = self.functions.setdefault(name, Function(name, []))
|
63
|
-
if commands:
|
64
|
-
# Process control flow immediately when commands are added
|
65
|
-
processed_commands = self._process_control_flow(name, commands)
|
66
|
-
fn.commands.extend(processed_commands)
|
67
|
-
return fn
|
68
|
-
|
69
|
-
def _process_control_flow(self, func_name: str, commands: List[str]) -> List[str]:
|
70
|
-
"""Process conditional blocks and loops in function commands and generate appropriate Minecraft commands."""
|
71
|
-
import re
|
72
|
-
|
73
|
-
processed_commands = []
|
74
|
-
i = 0
|
75
|
-
|
76
|
-
while i < len(commands):
|
77
|
-
cmd = commands[i].strip()
|
78
|
-
|
79
|
-
# Check for if statement
|
80
|
-
if_match = re.match(r'^if\s+"([^"]+)"\s*:\s*$', cmd)
|
81
|
-
if if_match:
|
82
|
-
condition = if_match.group(1)
|
83
|
-
if_commands = []
|
84
|
-
i += 1
|
85
|
-
|
86
|
-
# Collect commands for this if block (until next conditional or end)
|
87
|
-
while i < len(commands):
|
88
|
-
next_cmd = commands[i].strip()
|
89
|
-
# Stop if we hit another conditional or end of commands
|
90
|
-
if (re.match(r'^else\s+if\s+"', next_cmd) or
|
91
|
-
next_cmd == "else:" or
|
92
|
-
re.match(r'^if\s+"', next_cmd) or
|
93
|
-
re.match(r'^while\s+"', next_cmd) or
|
94
|
-
re.match(r'^for\s+', next_cmd)):
|
95
|
-
break
|
96
|
-
if next_cmd: # Skip empty lines
|
97
|
-
if_commands.append(next_cmd)
|
98
|
-
i += 1
|
99
|
-
|
100
|
-
# Generate conditional function
|
101
|
-
conditional_func_name = f"{func_name}_if_{len(processed_commands)}"
|
102
|
-
self.function(conditional_func_name, *if_commands)
|
103
|
-
|
104
|
-
# Add conditional execution command
|
105
|
-
processed_commands.append(f"execute if {condition} run function {self.name}:{conditional_func_name}")
|
106
|
-
continue
|
107
|
-
|
108
|
-
# Check for else if statement
|
109
|
-
elif_match = re.match(r'^else\s+if\s+"([^"]+)"\s*:\s*$', cmd)
|
110
|
-
if elif_match:
|
111
|
-
condition = elif_match.group(1)
|
112
|
-
|
113
|
-
# Convert comparison operators to matches syntax
|
114
|
-
condition = self._convert_comparison_operators(condition)
|
115
|
-
elif_commands = []
|
116
|
-
i += 1
|
117
|
-
|
118
|
-
# Collect commands for this else if block
|
119
|
-
while i < len(commands):
|
120
|
-
next_cmd = commands[i].strip()
|
121
|
-
if (re.match(r'^else\s+if\s+"', next_cmd) or
|
122
|
-
next_cmd == "else:" or
|
123
|
-
re.match(r'^if\s+"', next_cmd) or
|
124
|
-
re.match(r'^while\s+"', next_cmd) or
|
125
|
-
re.match(r'^for\s+', next_cmd)):
|
126
|
-
break
|
127
|
-
if next_cmd:
|
128
|
-
elif_commands.append(next_cmd)
|
129
|
-
i += 1
|
130
|
-
|
131
|
-
# Generate else if function
|
132
|
-
elif_func_name = f"{func_name}_elif_{len(processed_commands)}"
|
133
|
-
self.function(elif_func_name, *elif_commands)
|
134
|
-
|
135
|
-
# Add else if execution command
|
136
|
-
processed_commands.append(f"execute if {condition} run function {self.name}:{elif_func_name}")
|
137
|
-
continue
|
138
|
-
|
139
|
-
# Check for else statement
|
140
|
-
elif cmd == "else:":
|
141
|
-
else_commands = []
|
142
|
-
i += 1
|
143
|
-
|
144
|
-
# Collect commands for this else block
|
145
|
-
while i < len(commands):
|
146
|
-
next_cmd = commands[i].strip()
|
147
|
-
if (re.match(r'^if\s+"', next_cmd) or
|
148
|
-
re.match(r'^while\s+"', next_cmd) or
|
149
|
-
re.match(r'^for\s+', next_cmd)):
|
150
|
-
break
|
151
|
-
if next_cmd:
|
152
|
-
else_commands.append(next_cmd)
|
153
|
-
i += 1
|
154
|
-
|
155
|
-
# Generate else function
|
156
|
-
else_func_name = f"{func_name}_else_{len(processed_commands)}"
|
157
|
-
self.function(else_func_name, *else_commands)
|
158
|
-
|
159
|
-
# Add else execution command
|
160
|
-
processed_commands.append(f"execute run function {self.name}:{else_func_name}")
|
161
|
-
continue
|
162
|
-
|
163
|
-
# Check for while loop
|
164
|
-
while_match = re.match(r'^while\s+"([^"]+)"\s*:\s*$', cmd)
|
165
|
-
if while_match:
|
166
|
-
condition = while_match.group(1)
|
167
|
-
loop_commands = []
|
168
|
-
i += 1
|
169
|
-
|
170
|
-
# Collect commands for this while block
|
171
|
-
while i < len(commands):
|
172
|
-
next_cmd = commands[i].strip()
|
173
|
-
if (re.match(r'^if\s+"', next_cmd) or
|
174
|
-
re.match(r'^while\s+"', next_cmd) or
|
175
|
-
re.match(r'^for\s+', next_cmd)):
|
176
|
-
break
|
177
|
-
if next_cmd:
|
178
|
-
loop_commands.append(next_cmd)
|
179
|
-
i += 1
|
180
|
-
|
181
|
-
# Generate while loop function
|
182
|
-
loop_func_name = f"{func_name}_while_{len(processed_commands)}"
|
183
|
-
self.function(loop_func_name, *loop_commands)
|
184
|
-
|
185
|
-
# Generate while loop control function
|
186
|
-
loop_control_func_name = f"{func_name}_while_control_{len(processed_commands)}"
|
187
|
-
loop_control_commands = [
|
188
|
-
f"execute if {condition} run function {self.name}:{loop_func_name}",
|
189
|
-
f"execute if {condition} run function {self.name}:{loop_control_func_name}"
|
190
|
-
]
|
191
|
-
self.function(loop_control_func_name, *loop_control_commands)
|
192
|
-
|
193
|
-
# Add initial while loop call
|
194
|
-
processed_commands.append(f"execute if {condition} run function {self.name}:{loop_control_func_name}")
|
195
|
-
continue
|
196
|
-
|
197
|
-
# Check for for loop
|
198
|
-
for_match = re.match(r'^for\s+(\w+)\s+in\s+(.+?)\s*:\s*$', cmd)
|
199
|
-
if for_match:
|
200
|
-
var_name = for_match.group(1)
|
201
|
-
collection_name = for_match.group(2)
|
202
|
-
loop_commands = []
|
203
|
-
i += 1
|
204
|
-
|
205
|
-
# Collect ALL commands for this for block (including nested control structures)
|
206
|
-
while i < len(commands):
|
207
|
-
next_cmd = commands[i].strip()
|
208
|
-
# Stop if we hit another top-level control structure
|
209
|
-
if (re.match(r'^if\s+"', next_cmd) or
|
210
|
-
re.match(r'^else\s+if\s+"', next_cmd) or
|
211
|
-
next_cmd == "else:" or
|
212
|
-
re.match(r'^while\s+"', next_cmd) or
|
213
|
-
re.match(r'^for\s+', next_cmd)):
|
214
|
-
break
|
215
|
-
if next_cmd: # Skip empty lines
|
216
|
-
loop_commands.append(next_cmd)
|
217
|
-
i += 1
|
218
|
-
|
219
|
-
# Generate for loop function with processed conditionals
|
220
|
-
for_func_name = f"{func_name}_for_{len(processed_commands)}"
|
221
|
-
# Process the loop body commands to handle conditionals
|
222
|
-
processed_loop_commands = self._process_control_flow(for_func_name, loop_commands)
|
223
|
-
self.function(for_func_name, *processed_loop_commands)
|
224
|
-
|
225
|
-
# Generate for loop control function that iterates through collection
|
226
|
-
for_control_func_name = f"{func_name}_for_control_{len(processed_commands)}"
|
227
|
-
for_control_commands = [
|
228
|
-
f"execute as {collection_name} run function {self.name}:{for_func_name}"
|
229
|
-
]
|
230
|
-
self.function(for_control_func_name, *for_control_commands)
|
231
|
-
|
232
|
-
# Add initial for loop call
|
233
|
-
processed_commands.append(f"execute if entity {collection_name} run function {self.name}:{for_control_func_name}")
|
234
|
-
continue
|
235
|
-
|
236
|
-
# Regular command
|
237
|
-
processed_commands.append(cmd)
|
238
|
-
i += 1
|
239
|
-
|
240
|
-
return processed_commands
|
241
|
-
|
242
|
-
def recipe(self, name: str, data: dict) -> Recipe:
|
243
|
-
r = Recipe(name, data)
|
244
|
-
self.recipes[name] = r
|
245
|
-
return r
|
246
|
-
|
247
|
-
def advancement(self, name: str, data: dict) -> Advancement:
|
248
|
-
a = Advancement(name, data)
|
249
|
-
self.advancements[name] = a
|
250
|
-
return a
|
251
|
-
|
252
|
-
def loot_table(self, name: str, data: dict) -> LootTable:
|
253
|
-
lt = LootTable(name, data)
|
254
|
-
self.loot_tables[name] = lt
|
255
|
-
return lt
|
256
|
-
|
257
|
-
def predicate(self, name: str, data: dict) -> Predicate:
|
258
|
-
p = Predicate(name, data)
|
259
|
-
self.predicates[name] = p
|
260
|
-
return p
|
261
|
-
|
262
|
-
def item_modifier(self, name: str, data: dict) -> ItemModifier:
|
263
|
-
im = ItemModifier(name, data)
|
264
|
-
self.item_modifiers[name] = im
|
265
|
-
return im
|
266
|
-
|
267
|
-
def structure(self, name: str, data: dict) -> Structure:
|
268
|
-
s = Structure(name, data)
|
269
|
-
self.structures[name] = s
|
270
|
-
return s
|
271
|
-
|
272
|
-
@dataclass
|
273
|
-
class Tag:
|
274
|
-
registry: str # "function", "item", "block", "entity_type", "fluid", "game_event"
|
275
|
-
name: str # namespaced id e.g. "minecraft:tick" or "myns:my_tag"
|
276
|
-
values: List[str] = field(default_factory=list)
|
277
|
-
replace: bool = False
|
278
|
-
|
279
|
-
class Pack:
|
280
|
-
def __init__(self, name: str, description: str = "", pack_format: int = 48, min_format: Optional[Union[int, List[int]]] = None, max_format: Optional[Union[int, List[int]]] = None, min_engine_version: Optional[str] = None):
|
281
|
-
self.name = name
|
282
|
-
self.description = description or name
|
283
|
-
self.pack_format = pack_format
|
284
|
-
self.min_format = min_format
|
285
|
-
self.max_format = max_format
|
286
|
-
self.min_engine_version = min_engine_version
|
287
|
-
self.namespaces: Dict[str, Namespace] = {}
|
288
|
-
self.tags: List[Tag] = []
|
289
|
-
# helpful shortcuts
|
290
|
-
self._tick_functions: List[str] = []
|
291
|
-
self._load_functions: List[str] = []
|
292
|
-
|
293
|
-
# Namespace management
|
294
|
-
def namespace(self, name: str) -> Namespace:
|
295
|
-
ns = self.namespaces.get(name)
|
296
|
-
if ns is None:
|
297
|
-
ns = Namespace(name=name)
|
298
|
-
self.namespaces[name] = ns
|
299
|
-
return ns
|
300
|
-
|
301
|
-
# Function shortcuts
|
302
|
-
def fn(self, ns: str, path: str, *commands: str) -> Function:
|
303
|
-
return self.namespace(ns).function(path, *commands)
|
304
|
-
|
305
|
-
def on_tick(self, full_id: str):
|
306
|
-
"""Add a function id to minecraft:tick tag for running every tick."""
|
307
|
-
self._tick_functions.append(full_id)
|
308
|
-
|
309
|
-
def on_load(self, full_id: str):
|
310
|
-
"""Add a function id to minecraft:load tag for running on world load."""
|
311
|
-
self._load_functions.append(full_id)
|
312
|
-
|
313
|
-
# Tag builder
|
314
|
-
def tag(self, registry: str, name: str, values: Optional[List[str]] = None, replace: bool = False) -> Tag:
|
315
|
-
t = Tag(registry=registry, name=name, values=list(values or []), replace=replace)
|
316
|
-
self.tags.append(t)
|
317
|
-
return t
|
318
|
-
|
319
|
-
def _process_list_access_in_condition(self, condition: str, ns_name: str, func_name: str) -> str:
|
320
|
-
"""Process list access expressions in conditions and convert them to valid Minecraft syntax."""
|
321
|
-
import re
|
322
|
-
|
323
|
-
# Pattern to match list access expressions like list_name[index]
|
324
|
-
list_access_pattern = r'(\w+)\[(\w+)\]'
|
325
|
-
|
326
|
-
def replace_list_access(match):
|
327
|
-
list_name = match.group(1)
|
328
|
-
index_var = match.group(2)
|
329
|
-
|
330
|
-
# For now, just return the list name since we can't easily process this in conditions
|
331
|
-
# The actual list access will need to be handled in the CLI when processing variable assignments
|
332
|
-
return list_name
|
333
|
-
|
334
|
-
# Replace list access expressions in the condition
|
335
|
-
processed_condition = re.sub(list_access_pattern, replace_list_access, condition)
|
336
|
-
|
337
|
-
return processed_condition
|
338
|
-
|
339
|
-
def _convert_comparison_operators(self, condition: str) -> str:
|
340
|
-
"""Convert comparison operators to Minecraft matches syntax."""
|
341
|
-
processed_condition = condition
|
342
|
-
|
343
|
-
# Convert comparison operators to matches syntax
|
344
|
-
if ">=" in condition:
|
345
|
-
# score @s var >= 10 -> score @s var matches 10..
|
346
|
-
parts = condition.split(">=")
|
347
|
-
if len(parts) == 2:
|
348
|
-
left = parts[0].strip()
|
349
|
-
right = parts[1].strip()
|
350
|
-
processed_condition = f"{left} matches {right}.."
|
351
|
-
elif "<=" in condition:
|
352
|
-
# score @s var <= 10 -> score @s var matches ..10
|
353
|
-
parts = condition.split("<=")
|
354
|
-
if len(parts) == 2:
|
355
|
-
left = parts[0].strip()
|
356
|
-
right = parts[1].strip()
|
357
|
-
processed_condition = f"{left} matches ..{right}"
|
358
|
-
elif ">" in condition:
|
359
|
-
# score @s var > 10 -> score @s var matches 11..
|
360
|
-
parts = condition.split(">")
|
361
|
-
if len(parts) == 2:
|
362
|
-
left = parts[0].strip()
|
363
|
-
right = parts[1].strip()
|
364
|
-
try:
|
365
|
-
num = int(right)
|
366
|
-
processed_condition = f"{left} matches {num + 1}.."
|
367
|
-
except ValueError:
|
368
|
-
# If not a number, keep original
|
369
|
-
processed_condition = condition
|
370
|
-
elif "<" in condition:
|
371
|
-
# score @s var < 10 -> score @s var matches ..9
|
372
|
-
parts = condition.split("<")
|
373
|
-
if len(parts) == 2:
|
374
|
-
left = parts[0].strip()
|
375
|
-
right = parts[1].strip()
|
376
|
-
try:
|
377
|
-
num = int(right)
|
378
|
-
processed_condition = f"{left} matches ..{num - 1}"
|
379
|
-
except ValueError:
|
380
|
-
# If not a number, keep original
|
381
|
-
processed_condition = condition
|
382
|
-
|
383
|
-
# Convert string quotes for NBT data
|
384
|
-
if "data storage" in processed_condition and "'" in processed_condition:
|
385
|
-
processed_condition = processed_condition.replace("'", '"')
|
386
|
-
|
387
|
-
return processed_condition
|
388
|
-
|
389
|
-
def _process_control_flow(self, ns_name: str, func_name: str, commands: List[str]) -> List[str]:
|
390
|
-
"""Process conditional blocks and loops in function commands and generate appropriate Minecraft commands."""
|
391
|
-
import re
|
392
|
-
|
393
|
-
processed_commands = []
|
394
|
-
i = 0
|
395
|
-
previous_conditions = [] # Track conditions for proper else if logic
|
396
|
-
|
397
|
-
while i < len(commands):
|
398
|
-
cmd = commands[i].strip()
|
399
|
-
|
400
|
-
# Check for if statement
|
401
|
-
if_match = re.match(r'^if\s+"([^"]+)"\s*:\s*$', cmd)
|
402
|
-
if if_match:
|
403
|
-
condition = if_match.group(1)
|
404
|
-
|
405
|
-
# Process list access expressions in conditions
|
406
|
-
condition = self._process_list_access_in_condition(condition, ns_name, func_name)
|
407
|
-
|
408
|
-
# Convert comparison operators to matches syntax
|
409
|
-
condition = self._convert_comparison_operators(condition)
|
410
|
-
|
411
|
-
if_commands = []
|
412
|
-
i += 1
|
413
|
-
|
414
|
-
# Collect commands for this if block (until next conditional or end)
|
415
|
-
while i < len(commands):
|
416
|
-
next_cmd = commands[i].strip()
|
417
|
-
# Stop if we hit another conditional or end of commands
|
418
|
-
if (re.match(r'^else\s+if\s+"', next_cmd) or
|
419
|
-
next_cmd == "else:" or
|
420
|
-
re.match(r'^if\s+"', next_cmd) or
|
421
|
-
re.match(r'^while\s+"', next_cmd) or
|
422
|
-
re.match(r'^for\s+', next_cmd)):
|
423
|
-
break
|
424
|
-
if next_cmd: # Skip empty lines
|
425
|
-
if_commands.append(next_cmd)
|
426
|
-
i += 1
|
427
|
-
|
428
|
-
# Generate conditional function
|
429
|
-
conditional_func_name = f"{func_name}_if_{len(processed_commands)}"
|
430
|
-
self.namespace(ns_name).function(conditional_func_name, *if_commands)
|
431
|
-
|
432
|
-
# Add execute command
|
433
|
-
processed_commands.append(f"execute if {condition} run function {ns_name}:{conditional_func_name}")
|
434
|
-
previous_conditions = [condition] # Reset for new if chain
|
435
|
-
continue
|
436
|
-
|
437
|
-
# Check for else if statement
|
438
|
-
elif_match = re.match(r'^else\s+if\s+"([^"]+)"\s*:\s*$', cmd)
|
439
|
-
if elif_match:
|
440
|
-
condition = elif_match.group(1)
|
441
|
-
elif_commands = []
|
442
|
-
i += 1
|
443
|
-
|
444
|
-
# Collect commands for this else if block (until next conditional or end)
|
445
|
-
while i < len(commands):
|
446
|
-
next_cmd = commands[i].strip()
|
447
|
-
# Stop if we hit another conditional or end of commands
|
448
|
-
if (re.match(r'^else\s+if\s+"', next_cmd) or
|
449
|
-
next_cmd == "else:" or
|
450
|
-
re.match(r'^if\s+"', next_cmd) or
|
451
|
-
re.match(r'^while\s+"', next_cmd) or
|
452
|
-
re.match(r'^for\s+', next_cmd)):
|
453
|
-
break
|
454
|
-
if next_cmd: # Skip empty lines
|
455
|
-
elif_commands.append(next_cmd)
|
456
|
-
i += 1
|
457
|
-
|
458
|
-
# Generate conditional function
|
459
|
-
conditional_func_name = f"{func_name}_elif_{len(processed_commands)}"
|
460
|
-
self.namespace(ns_name).function(conditional_func_name, *elif_commands)
|
461
|
-
|
462
|
-
# Build execute command with all previous conditions negated
|
463
|
-
execute_parts = []
|
464
|
-
for prev_condition in previous_conditions:
|
465
|
-
execute_parts.append(f"unless {prev_condition}")
|
466
|
-
execute_parts.append(f"if {condition}")
|
467
|
-
execute_parts.append(f"run function {ns_name}:{conditional_func_name}")
|
468
|
-
|
469
|
-
processed_commands.append("execute " + " ".join(execute_parts))
|
470
|
-
previous_conditions.append(condition)
|
471
|
-
continue
|
472
|
-
|
473
|
-
# Check for else statement
|
474
|
-
elif cmd == "else:":
|
475
|
-
else_commands = []
|
476
|
-
i += 1
|
477
|
-
|
478
|
-
# Collect commands for this else block (until end)
|
479
|
-
while i < len(commands):
|
480
|
-
next_cmd = commands[i].strip()
|
481
|
-
# Stop if we hit another conditional or end of commands
|
482
|
-
if (re.match(r'^else\s+if\s+"', next_cmd) or
|
483
|
-
re.match(r'^if\s+"', next_cmd) or
|
484
|
-
re.match(r'^while\s+"', next_cmd) or
|
485
|
-
re.match(r'^for\s+', next_cmd)):
|
486
|
-
break
|
487
|
-
if next_cmd: # Skip empty lines
|
488
|
-
else_commands.append(next_cmd)
|
489
|
-
i += 1
|
490
|
-
|
491
|
-
# Generate conditional function
|
492
|
-
conditional_func_name = f"{func_name}_else"
|
493
|
-
self.namespace(ns_name).function(conditional_func_name, *else_commands)
|
494
|
-
|
495
|
-
# Build execute command with all previous conditions negated
|
496
|
-
execute_parts = []
|
497
|
-
for prev_condition in previous_conditions:
|
498
|
-
execute_parts.append(f"unless {prev_condition}")
|
499
|
-
execute_parts.append(f"run function {ns_name}:{conditional_func_name}")
|
500
|
-
|
501
|
-
processed_commands.append("execute " + " ".join(execute_parts))
|
502
|
-
previous_conditions = [] # Reset for next if chain
|
503
|
-
continue
|
504
|
-
|
505
|
-
# Check for while loop
|
506
|
-
while_match = re.match(r'^while\s+"([^"]+)"\s*:\s*$', cmd)
|
507
|
-
if while_match:
|
508
|
-
condition = while_match.group(1)
|
509
|
-
loop_commands = []
|
510
|
-
i += 1
|
511
|
-
|
512
|
-
# Collect commands for this while block (until end or next control structure)
|
513
|
-
while i < len(commands):
|
514
|
-
next_cmd = commands[i].strip()
|
515
|
-
# Stop if we hit another control structure
|
516
|
-
if (re.match(r'^if\s+"', next_cmd) or
|
517
|
-
re.match(r'^else\s+if\s+"', next_cmd) or
|
518
|
-
next_cmd == "else:" or
|
519
|
-
re.match(r'^while\s+"', next_cmd) or
|
520
|
-
re.match(r'^for\s+', next_cmd)):
|
521
|
-
break
|
522
|
-
if next_cmd: # Skip empty lines
|
523
|
-
loop_commands.append(next_cmd)
|
524
|
-
i += 1
|
525
|
-
|
526
|
-
# Generate loop function
|
527
|
-
loop_func_name = f"{func_name}_while_{len(processed_commands)}"
|
528
|
-
self.namespace(ns_name).function(loop_func_name, *loop_commands)
|
529
|
-
|
530
|
-
# Generate loop control function that calls itself if condition is still true
|
531
|
-
loop_control_func_name = f"{func_name}_while_control_{len(processed_commands)}"
|
532
|
-
loop_control_commands = [
|
533
|
-
f"execute if {condition} run function {ns_name}:{loop_func_name}",
|
534
|
-
f"execute if {condition} run function {ns_name}:{loop_control_func_name}"
|
535
|
-
]
|
536
|
-
self.namespace(ns_name).function(loop_control_func_name, *loop_control_commands)
|
537
|
-
|
538
|
-
# Add initial loop call
|
539
|
-
processed_commands.append(f"execute if {condition} run function {ns_name}:{loop_control_func_name}")
|
540
|
-
continue
|
541
|
-
|
542
|
-
# Check for for loop
|
543
|
-
for_match = re.match(r'^for\s+(\w+)\s+in\s+(.+?)\s*:\s*$', cmd)
|
544
|
-
if for_match:
|
545
|
-
var_name = for_match.group(1)
|
546
|
-
collection_name = for_match.group(2)
|
547
|
-
loop_commands = []
|
548
|
-
i += 1
|
549
|
-
|
550
|
-
# Collect commands for this for block (including nested control structures)
|
551
|
-
while i < len(commands):
|
552
|
-
next_cmd = commands[i].strip()
|
553
|
-
# Stop if we hit another top-level control structure (same indentation level)
|
554
|
-
if (re.match(r'^if\s+"', next_cmd) or
|
555
|
-
re.match(r'^else\s+if\s+"', next_cmd) or
|
556
|
-
next_cmd == "else:" or
|
557
|
-
re.match(r'^while\s+"', next_cmd) or
|
558
|
-
re.match(r'^for\s+', next_cmd)) and not next_cmd.startswith(' '):
|
559
|
-
break
|
560
|
-
if next_cmd: # Skip empty lines
|
561
|
-
loop_commands.append(next_cmd)
|
562
|
-
i += 1
|
563
|
-
|
564
|
-
# Generate for loop function with processed conditionals
|
565
|
-
for_func_name = f"{func_name}_for_{len(processed_commands)}"
|
566
|
-
# Process the loop body commands to handle conditionals
|
567
|
-
processed_loop_commands = self._process_conditionals(loop_commands, for_func_name, ns_name)
|
568
|
-
self.namespace(ns_name).function(for_func_name, *processed_loop_commands)
|
569
|
-
|
570
|
-
# Generate for loop control function that iterates through collection
|
571
|
-
for_control_func_name = f"{func_name}_for_control_{len(processed_commands)}"
|
572
|
-
for_control_commands = [
|
573
|
-
f"execute as {collection_name} run function {ns_name}:{for_func_name}"
|
574
|
-
]
|
575
|
-
self.namespace(ns_name).function(for_control_func_name, *for_control_commands)
|
576
|
-
|
577
|
-
# Add initial for loop call
|
578
|
-
processed_commands.append(f"execute if entity {collection_name} run function {ns_name}:{for_control_func_name}")
|
579
|
-
continue
|
580
|
-
|
581
|
-
# Regular command
|
582
|
-
processed_commands.append(cmd)
|
583
|
-
i += 1
|
584
|
-
|
585
|
-
return processed_commands
|
586
|
-
|
587
|
-
def merge(self, other: "Pack"):
|
588
|
-
"""Merge content of another Pack into this one. Raises on conflicting function names within same namespace."""
|
589
|
-
# Preserve metadata from the root pack (self) - don't override with other pack's metadata
|
590
|
-
# This ensures that the first pack's metadata (min_format, max_format, min_engine_version) is preserved
|
591
|
-
|
592
|
-
# Namespaces
|
593
|
-
for ns_name, ns_other in other.namespaces.items():
|
594
|
-
ns_self = self.namespaces.get(ns_name)
|
595
|
-
if ns_self is None:
|
596
|
-
self.namespaces[ns_name] = ns_other
|
597
|
-
continue
|
598
|
-
# functions
|
599
|
-
for fname, fobj in ns_other.functions.items():
|
600
|
-
if fname in ns_self.functions:
|
601
|
-
raise ValueError(f"Duplicate function '{ns_name}:{fname}' while merging")
|
602
|
-
ns_self.functions[fname] = fobj
|
603
|
-
# simple maps
|
604
|
-
ns_self.recipes.update(ns_other.recipes)
|
605
|
-
ns_self.advancements.update(ns_other.advancements)
|
606
|
-
ns_self.loot_tables.update(ns_other.loot_tables)
|
607
|
-
ns_self.predicates.update(ns_other.predicates)
|
608
|
-
ns_self.item_modifiers.update(ns_other.item_modifiers)
|
609
|
-
ns_self.structures.update(ns_other.structures)
|
610
|
-
|
611
|
-
# Tags and hooks
|
612
|
-
self.tags.extend(other.tags)
|
613
|
-
self._tick_functions.extend(other._tick_functions)
|
614
|
-
self._load_functions.extend(other._load_functions)
|
615
|
-
|
616
|
-
# Compilation
|
617
|
-
def build(self, out_dir: str):
|
618
|
-
dm: DirMap = get_dir_map(self.pack_format)
|
619
|
-
|
620
|
-
# pack.mcmeta
|
621
|
-
pack_meta = {"description": self.description}
|
622
|
-
|
623
|
-
# Handle pack format metadata based on version
|
624
|
-
if self.pack_format < 82:
|
625
|
-
# For older formats, use pack_format and supported_formats
|
626
|
-
pack_meta["pack_format"] = self.pack_format
|
627
|
-
if hasattr(self, 'supported_formats') and self.supported_formats:
|
628
|
-
pack_meta["supported_formats"] = self.supported_formats
|
629
|
-
else:
|
630
|
-
# For format 82+, use min_format and max_format
|
631
|
-
if self.min_format is not None:
|
632
|
-
pack_meta["min_format"] = self.min_format
|
633
|
-
if self.max_format is not None:
|
634
|
-
pack_meta["max_format"] = self.max_format
|
635
|
-
# pack_format is optional for 82+
|
636
|
-
if self.pack_format:
|
637
|
-
pack_meta["pack_format"] = self.pack_format
|
638
|
-
|
639
|
-
# Add engine version if specified
|
640
|
-
if self.min_engine_version:
|
641
|
-
pack_meta["min_engine_version"] = self.min_engine_version
|
642
|
-
|
643
|
-
mcmeta = {"pack": pack_meta}
|
644
|
-
write_json(os.path.join(out_dir, "pack.mcmeta"), mcmeta)
|
645
|
-
|
646
|
-
data_root = os.path.join(out_dir, "data")
|
647
|
-
ensure_dir(data_root)
|
648
|
-
|
649
|
-
# Namespaces
|
650
|
-
for ns_name, ns in self.namespaces.items():
|
651
|
-
ns_root = os.path.join(data_root, ns_name)
|
652
|
-
# Functions
|
653
|
-
functions_to_process = list(ns.functions.items())
|
654
|
-
processed_functions = set()
|
655
|
-
generated_functions = set() # Track functions created during conditional processing
|
656
|
-
|
657
|
-
for path, fn in functions_to_process:
|
658
|
-
fn_dir = os.path.join(ns_root, dm.function, os.path.dirname(path))
|
659
|
-
file_path = os.path.join(ns_root, dm.function, f"{path}.mcfunction")
|
660
|
-
ensure_dir(fn_dir)
|
661
|
-
|
662
|
-
# Process conditionals in function commands
|
663
|
-
print(f"Processing function: {ns_name}:{path}")
|
664
|
-
# Check if commands are already in new format (no semicolons, no old-style control flow)
|
665
|
-
if any(cmd.endswith(';') for cmd in fn.commands):
|
666
|
-
# Old format - process with control flow
|
667
|
-
processed_commands = self._process_control_flow(ns_name, path, fn.commands)
|
668
|
-
else:
|
669
|
-
# New format - commands are already processed
|
670
|
-
processed_commands = fn.commands
|
671
|
-
write_text(file_path, "\n".join(processed_commands))
|
672
|
-
processed_functions.add(path)
|
673
|
-
|
674
|
-
# Track any new functions that were created during conditional processing
|
675
|
-
for new_path in ns.functions.keys():
|
676
|
-
if new_path not in [f[0] for f in functions_to_process]:
|
677
|
-
generated_functions.add(new_path)
|
678
|
-
|
679
|
-
# Write any additional functions created during conditional processing
|
680
|
-
for path, fn in ns.functions.items():
|
681
|
-
if path not in processed_functions and path in generated_functions: # Only write generated functions
|
682
|
-
fn_dir = os.path.join(ns_root, dm.function, os.path.dirname(path))
|
683
|
-
file_path = os.path.join(ns_root, dm.function, f"{path}.mcfunction")
|
684
|
-
ensure_dir(fn_dir)
|
685
|
-
# Process loops in generated functions (conditionals are already processed)
|
686
|
-
if any(cmd.endswith(';') for cmd in fn.commands):
|
687
|
-
# Old format - process with control flow
|
688
|
-
processed_commands = self._process_control_flow(ns_name, path, fn.commands)
|
689
|
-
else:
|
690
|
-
# New format - commands are already processed
|
691
|
-
processed_commands = fn.commands
|
692
|
-
write_text(file_path, "\n".join(processed_commands))
|
693
|
-
|
694
|
-
# Recipes, Advancements, etc.
|
695
|
-
for name, r in ns.recipes.items():
|
696
|
-
recipe_dir = os.path.join(ns_root, dm.recipe)
|
697
|
-
ensure_dir(recipe_dir)
|
698
|
-
write_json(os.path.join(recipe_dir, f"{name}.json"), r.data)
|
699
|
-
for name, a in ns.advancements.items():
|
700
|
-
advancement_dir = os.path.join(ns_root, dm.advancement)
|
701
|
-
ensure_dir(advancement_dir)
|
702
|
-
write_json(os.path.join(advancement_dir, f"{name}.json"), a.data)
|
703
|
-
for name, lt in ns.loot_tables.items():
|
704
|
-
loot_table_dir = os.path.join(ns_root, dm.loot_table)
|
705
|
-
ensure_dir(loot_table_dir)
|
706
|
-
write_json(os.path.join(loot_table_dir, f"{name}.json"), lt.data)
|
707
|
-
for name, p in ns.predicates.items():
|
708
|
-
predicate_dir = os.path.join(ns_root, dm.predicate)
|
709
|
-
ensure_dir(predicate_dir)
|
710
|
-
write_json(os.path.join(predicate_dir, f"{name}.json"), p.data)
|
711
|
-
for name, im in ns.item_modifiers.items():
|
712
|
-
item_modifier_dir = os.path.join(ns_root, dm.item_modifier)
|
713
|
-
ensure_dir(item_modifier_dir)
|
714
|
-
write_json(os.path.join(item_modifier_dir, f"{name}.json"), im.data)
|
715
|
-
for name, s in ns.structures.items():
|
716
|
-
# Structure typically NBT; here we store JSON placeholder
|
717
|
-
structure_dir = os.path.join(ns_root, dm.structure)
|
718
|
-
ensure_dir(structure_dir)
|
719
|
-
write_json(os.path.join(structure_dir, f"{name}.json"), s.data)
|
720
|
-
|
721
|
-
# Autowire special function tags
|
722
|
-
print(f"DEBUG: _tick_functions: {self._tick_functions}")
|
723
|
-
print(f"DEBUG: _load_functions: {self._load_functions}")
|
724
|
-
if self._tick_functions:
|
725
|
-
self.tags.append(Tag("function", "minecraft:tick", values=self._tick_functions))
|
726
|
-
if self._load_functions:
|
727
|
-
self.tags.append(Tag("function", "minecraft:load", values=self._load_functions))
|
728
|
-
|
729
|
-
# Debug: Print all tags before processing
|
730
|
-
print(f"DEBUG: Pack has {len(self.tags)} tags before processing:")
|
731
|
-
for i, tag in enumerate(self.tags):
|
732
|
-
print(f"DEBUG: Tag {i}: registry={tag.registry}, name={tag.name}, values={tag.values}")
|
733
|
-
|
734
|
-
# Tags
|
735
|
-
print("DEBUG: About to process tags in Pack.build()")
|
736
|
-
for t in self.tags:
|
737
|
-
print(f"DEBUG: Processing tag: registry={t.registry}, name={t.name}")
|
738
|
-
if ":" not in t.name:
|
739
|
-
raise ValueError(f"Tag name must be namespaced (e.g., 'minecraft:tick'), got {t.name}. Tag registry: {t.registry}, values: {t.values}")
|
740
|
-
ns, path = t.name.split(":", 1)
|
741
|
-
|
742
|
-
if t.registry == "function":
|
743
|
-
tag_path = dm.tags_function
|
744
|
-
elif t.registry == "item":
|
745
|
-
tag_path = dm.tags_item
|
746
|
-
elif t.registry == "block":
|
747
|
-
tag_path = dm.tags_block
|
748
|
-
elif t.registry == "entity_type":
|
749
|
-
tag_path = dm.tags_entity_type
|
750
|
-
elif t.registry == "fluid":
|
751
|
-
tag_path = dm.tags_fluid
|
752
|
-
elif t.registry == "game_event":
|
753
|
-
tag_path = dm.tags_game_event
|
754
|
-
else:
|
755
|
-
raise ValueError(f"Unknown tag registry: {t.registry}")
|
756
|
-
|
757
|
-
tag_obj = {"replace": t.replace, "values": t.values}
|
758
|
-
write_json(os.path.join(data_root, ns, tag_path, f"{path}.json"), tag_obj)
|