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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. minecraft_datapack_language/__init__.py +17 -2
  2. minecraft_datapack_language/_version.py +2 -2
  3. minecraft_datapack_language/ast_nodes.py +87 -59
  4. minecraft_datapack_language/mdl_compiler.py +470 -0
  5. minecraft_datapack_language/mdl_errors.py +14 -0
  6. minecraft_datapack_language/mdl_lexer.py +624 -0
  7. minecraft_datapack_language/mdl_parser.py +573 -0
  8. minecraft_datapack_language-15.4.29.dist-info/METADATA +266 -0
  9. minecraft_datapack_language-15.4.29.dist-info/RECORD +16 -0
  10. minecraft_datapack_language/cli.py +0 -159
  11. minecraft_datapack_language/cli_build.py +0 -1292
  12. minecraft_datapack_language/cli_check.py +0 -155
  13. minecraft_datapack_language/cli_colors.py +0 -264
  14. minecraft_datapack_language/cli_help.py +0 -508
  15. minecraft_datapack_language/cli_new.py +0 -300
  16. minecraft_datapack_language/cli_utils.py +0 -276
  17. minecraft_datapack_language/expression_processor.py +0 -352
  18. minecraft_datapack_language/linter.py +0 -409
  19. minecraft_datapack_language/mdl_lexer_js.py +0 -754
  20. minecraft_datapack_language/mdl_parser_js.py +0 -1049
  21. minecraft_datapack_language/pack.py +0 -758
  22. minecraft_datapack_language-15.4.28.dist-info/METADATA +0 -1274
  23. minecraft_datapack_language-15.4.28.dist-info/RECORD +0 -25
  24. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/WHEEL +0 -0
  25. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/entry_points.txt +0 -0
  26. {minecraft_datapack_language-15.4.28.dist-info → minecraft_datapack_language-15.4.29.dist-info}/licenses/LICENSE +0 -0
  27. {minecraft_datapack_language-15.4.28.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)