redscript-mc 1.0.0 → 1.1.0

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 (106) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +58 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +10 -10
  9. package/dist/__tests__/codegen.test.js +1 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +146 -5
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lowering.test.js +36 -3
  15. package/dist/__tests__/mc-integration.test.js +255 -10
  16. package/dist/__tests__/mc-syntax.test.js +3 -3
  17. package/dist/__tests__/nbt.test.js +2 -2
  18. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  19. package/dist/__tests__/runtime.test.js +1 -1
  20. package/dist/ast/types.d.ts +21 -3
  21. package/dist/cli.js +25 -7
  22. package/dist/codegen/mcfunction/index.d.ts +1 -1
  23. package/dist/codegen/mcfunction/index.js +8 -2
  24. package/dist/codegen/structure/index.js +7 -1
  25. package/dist/formatter/index.d.ts +1 -0
  26. package/dist/formatter/index.js +26 -0
  27. package/dist/ir/builder.d.ts +2 -1
  28. package/dist/ir/types.d.ts +7 -2
  29. package/dist/ir/types.js +1 -1
  30. package/dist/lowering/index.d.ts +2 -0
  31. package/dist/lowering/index.js +183 -8
  32. package/dist/mc-test/runner.d.ts +2 -2
  33. package/dist/mc-test/runner.js +3 -3
  34. package/dist/mc-test/setup.js +2 -2
  35. package/dist/parser/index.d.ts +2 -0
  36. package/dist/parser/index.js +75 -7
  37. package/docs/COMPILATION_STATS.md +24 -24
  38. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  39. package/docs/STRUCTURE_TARGET.md +1 -1
  40. package/editors/vscode/.vscodeignore +1 -0
  41. package/editors/vscode/icons/mcrs.svg +7 -0
  42. package/editors/vscode/icons/redscript-icons.json +10 -0
  43. package/editors/vscode/out/extension.js +152 -9
  44. package/editors/vscode/package.json +10 -3
  45. package/editors/vscode/src/hover.ts +55 -2
  46. package/editors/vscode/src/symbols.ts +42 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/cli.test.ts +10 -10
  49. package/src/__tests__/codegen.test.ts +1 -1
  50. package/src/__tests__/diagnostics.test.ts +5 -5
  51. package/src/__tests__/e2e.test.ts +134 -5
  52. package/src/__tests__/lowering.test.ts +48 -3
  53. package/src/__tests__/mc-integration.test.ts +285 -10
  54. package/src/__tests__/mc-syntax.test.ts +3 -3
  55. package/src/__tests__/nbt.test.ts +2 -2
  56. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  57. package/src/__tests__/runtime.test.ts +1 -1
  58. package/src/ast/types.ts +20 -3
  59. package/src/cli.ts +10 -10
  60. package/src/codegen/mcfunction/index.ts +9 -2
  61. package/src/codegen/structure/index.ts +8 -1
  62. package/src/examples/capture_the_flag.mcrs +208 -0
  63. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  64. package/src/examples/hunger_games.mcrs +301 -0
  65. package/src/examples/new_features_demo.mcrs +193 -0
  66. package/src/examples/parkour_race.mcrs +233 -0
  67. package/src/examples/rpg.mcrs +13 -0
  68. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  69. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  70. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  71. package/src/examples/zombie_survival.mcrs +314 -0
  72. package/src/ir/builder.ts +3 -1
  73. package/src/ir/types.ts +8 -2
  74. package/src/lowering/index.ts +156 -8
  75. package/src/mc-test/runner.ts +3 -3
  76. package/src/mc-test/setup.ts +2 -2
  77. package/src/parser/index.ts +81 -8
  78. package/src/stdlib/README.md +155 -147
  79. package/src/stdlib/bossbar.mcrs +68 -0
  80. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  81. package/src/stdlib/effects.mcrs +64 -0
  82. package/src/stdlib/interactions.mcrs +195 -0
  83. package/src/stdlib/inventory.mcrs +38 -0
  84. package/src/stdlib/mobs.mcrs +99 -0
  85. package/src/stdlib/particles.mcrs +52 -0
  86. package/src/stdlib/sets.mcrs +20 -0
  87. package/src/stdlib/spawn.mcrs +41 -0
  88. package/src/stdlib/teams.mcrs +68 -0
  89. package/src/stdlib/world.mcrs +92 -0
  90. package/src/examples/rpg.rs +0 -13
  91. package/src/stdlib/mobs.rs +0 -99
  92. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  93. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  94. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  95. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  96. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  97. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  98. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  99. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  100. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  101. /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
  102. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  103. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  104. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  105. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  106. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -65,12 +65,12 @@ beforeAll(async () => {
65
65
  }
66
66
 
67
67
  // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
68
- // counter.rs
69
- if (fs.existsSync(path.join(__dirname, '../examples/counter.rs'))) {
70
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.rs'), 'utf-8'), 'counter')
68
+ // counter.mcrs
69
+ if (fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))) {
70
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8'), 'counter')
71
71
  }
72
- if (fs.existsSync(path.join(__dirname, '../examples/world_manager.rs'))) {
73
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.rs'), 'utf-8'), 'world_manager')
72
+ if (fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))) {
73
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.mcrs'), 'utf-8'), 'world_manager')
74
74
  }
75
75
  writeFixture(`
76
76
  @tick
@@ -154,12 +154,118 @@ beforeAll(async () => {
154
154
  }
155
155
  `, 'fill_test')
156
156
 
157
+ // Scenario E: for-range loop — loop counter increments exactly N times
158
+ writeFixture(`
159
+ fn count_to_five() {
160
+ scoreboard_set("#range", "counter", 0);
161
+ for i in 0..5 {
162
+ let c: int = scoreboard_get("#range", "counter");
163
+ scoreboard_set("#range", "counter", c + 1);
164
+ }
165
+ }
166
+ `, 'range_test')
167
+
168
+ // Scenario F: function call with return value — verifies $ret propagation
169
+ writeFixture(`
170
+ fn triple(x: int) -> int {
171
+ return x * 3;
172
+ }
173
+ fn run_nested() {
174
+ let a: int = triple(4);
175
+ scoreboard_set("#nested", "result", a);
176
+ }
177
+ `, 'nested_test')
178
+
179
+ // Scenario G: match statement dispatches to correct branch
180
+ writeFixture(`
181
+ fn classify(x: int) {
182
+ match (x) {
183
+ 1 => { scoreboard_set("#match", "out", 10); }
184
+ 2 => { scoreboard_set("#match", "out", 20); }
185
+ 3 => { scoreboard_set("#match", "out", 30); }
186
+ _ => { scoreboard_set("#match", "out", -1); }
187
+ }
188
+ }
189
+ `, 'match_test')
190
+
191
+ // Scenario H: while loop counts down
192
+ writeFixture(`
193
+ fn countdown() {
194
+ scoreboard_set("#wloop", "i", 10);
195
+ scoreboard_set("#wloop", "steps", 0);
196
+ let i: int = scoreboard_get("#wloop", "i");
197
+ while (i > 0) {
198
+ let s: int = scoreboard_get("#wloop", "steps");
199
+ scoreboard_set("#wloop", "steps", s + 1);
200
+ i = i - 1;
201
+ scoreboard_set("#wloop", "i", i);
202
+ }
203
+ }
204
+ `, 'while_test')
205
+
206
+ // Scenario I: multiple if/else branches (boundary test)
207
+ writeFixture(`
208
+ fn classify_score() {
209
+ let x: int = scoreboard_get("#boundary", "input");
210
+ if (x > 100) {
211
+ scoreboard_set("#boundary", "tier", 3);
212
+ } else {
213
+ if (x > 50) {
214
+ scoreboard_set("#boundary", "tier", 2);
215
+ } else {
216
+ if (x > 0) {
217
+ scoreboard_set("#boundary", "tier", 1);
218
+ } else {
219
+ scoreboard_set("#boundary", "tier", 0);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ `, 'boundary_test')
225
+
226
+ // Scenario J: entity management — summon via raw commands
227
+ writeFixture(`
228
+ fn tag_entities() {
229
+ raw("summon minecraft:armor_stand 10 65 10");
230
+ raw("summon minecraft:armor_stand 11 65 10");
231
+ raw("summon minecraft:armor_stand 12 65 10");
232
+ }
233
+ `, 'tag_test')
234
+
235
+ // Scenario K: mixed arithmetic — order of operations
236
+ writeFixture(`
237
+ fn math_order() {
238
+ let a: int = 2;
239
+ let b: int = 3;
240
+ let c: int = 4;
241
+ scoreboard_set("#order", "r1", a + b * c);
242
+ scoreboard_set("#order", "r2", (a + b) * c);
243
+ let d: int = 100;
244
+ let e: int = d / 3;
245
+ scoreboard_set("#order", "r3", e);
246
+ }
247
+ `, 'order_test')
248
+
249
+ // Scenario L: scoreboard read-modify-write chain
250
+ writeFixture(`
251
+ fn chain_rmw() {
252
+ scoreboard_set("#rmw", "v", 1);
253
+ let v: int = scoreboard_get("#rmw", "v");
254
+ scoreboard_set("#rmw", "v", v * 2);
255
+ v = scoreboard_get("#rmw", "v");
256
+ scoreboard_set("#rmw", "v", v * 2);
257
+ v = scoreboard_get("#rmw", "v");
258
+ scoreboard_set("#rmw", "v", v * 2);
259
+ }
260
+ `, 'rmw_test')
261
+
157
262
  // ── Full reset + safe data reload ────────────────────────────────────
158
263
  await mc.fullReset()
159
264
 
160
265
  // Pre-create scoreboards
161
266
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
162
- 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val']) {
267
+ 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
268
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v']) {
163
269
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => {})
164
270
  }
165
271
  await mc.command('/scoreboard players set counter ticks 0')
@@ -192,7 +298,7 @@ describe('MC Integration Tests', () => {
192
298
  })
193
299
 
194
300
  // ─── Test 2: Counter tick ─────────────────────────────────────────────
195
- test('counter.rs: tick function increments scoreboard over time', async () => {
301
+ test('counter.mcrs: tick function increments scoreboard over time', async () => {
196
302
  if (!serverOnline) return
197
303
 
198
304
  await mc.ticks(40) // Wait 2s (counter was already init'd in beforeAll)
@@ -202,7 +308,7 @@ describe('MC Integration Tests', () => {
202
308
  })
203
309
 
204
310
  // ─── Test 3: setblock ────────────────────────────────────────────────
205
- test('world_manager.rs: setblock places correct block', async () => {
311
+ test('world_manager.mcrs: setblock places correct block', async () => {
206
312
  if (!serverOnline) return
207
313
 
208
314
  // Clear just the lobby area, keep other state
@@ -217,7 +323,7 @@ describe('MC Integration Tests', () => {
217
323
  })
218
324
 
219
325
  // ─── Test 4: fill ────────────────────────────────────────────────────
220
- test('world_manager.rs: fill creates smooth_stone floor', async () => {
326
+ test('world_manager.mcrs: fill creates smooth_stone floor', async () => {
221
327
  if (!serverOnline) return
222
328
  // Runs after test 3, floor should still be there
223
329
  const block = await mc.block(4, 64, 4)
@@ -344,7 +450,7 @@ describe('E2E Scenario Tests', () => {
344
450
  })
345
451
 
346
452
  // Scenario B: No temp var collision between two functions called in sequence
347
- // Verifies: each function's $t0/$t1 temp vars are isolated per-call, not globally shared
453
+ // Verifies: each function's temp vars are isolated per-call via globally unique names
348
454
  // If there's a bug, calc_product would see sum's leftover $t vars and produce wrong result
349
455
  test('B: calc_sum + calc_product called in sequence — no temp var collision', async () => {
350
456
  if (!serverOnline) return
@@ -406,4 +512,173 @@ describe('E2E Scenario Tests', () => {
406
512
  console.log(` fill_test: blocks [0-3,70,0]=stone, [-1]/[4]=air ✓`)
407
513
  })
408
514
 
515
+ // Scenario E: for-range loop executes body exactly N times
516
+ // Verifies: for i in 0..5 increments counter 5 times
517
+ test('E: for-range loop increments counter exactly 5 times', async () => {
518
+ if (!serverOnline) return
519
+
520
+ await mc.command('/function range_test:__load')
521
+ await mc.command('/function range_test:count_to_five')
522
+ await mc.ticks(10)
523
+
524
+ const counter = await mc.scoreboard('#range', 'counter')
525
+ expect(counter).toBe(5)
526
+ console.log(` for-range 0..5 → counter=${counter} (expect 5) ✓`)
527
+ })
528
+
529
+ // Scenario F: function return value propagation
530
+ // Verifies: $ret from callee is correctly captured in caller's variable
531
+ test('F: function return value — triple(4) = 12', async () => {
532
+ if (!serverOnline) return
533
+
534
+ await mc.command('/function nested_test:__load')
535
+ await mc.command('/function nested_test:run_nested')
536
+ await mc.ticks(10)
537
+
538
+ const result = await mc.scoreboard('#nested', 'result')
539
+ expect(result).toBe(12) // triple(4) = 4*3 = 12
540
+ console.log(` triple(4) = ${result} (expect 12) ✓`)
541
+ })
542
+
543
+ // Scenario G: match dispatches to correct branch
544
+ // Verifies: match statement selects right arm for values 1, 2, 3, and default
545
+ test('G: match statement dispatches to correct branch', async () => {
546
+ if (!serverOnline) return
547
+
548
+ await mc.command('/function match_test:__load')
549
+
550
+ // Test match on value 2
551
+ await mc.command('/scoreboard players set $p0 rs 2')
552
+ await mc.command('/function match_test:classify')
553
+ await mc.ticks(5)
554
+ let out = await mc.scoreboard('#match', 'out')
555
+ expect(out).toBe(20)
556
+ console.log(` match(2) → out=${out} (expect 20) ✓`)
557
+
558
+ // Test match on value 3
559
+ await mc.command('/scoreboard players set $p0 rs 3')
560
+ await mc.command('/function match_test:classify')
561
+ await mc.ticks(5)
562
+ out = await mc.scoreboard('#match', 'out')
563
+ expect(out).toBe(30)
564
+ console.log(` match(3) → out=${out} (expect 30) ✓`)
565
+
566
+ // Test default branch (value 99)
567
+ await mc.command('/scoreboard players set $p0 rs 99')
568
+ await mc.command('/function match_test:classify')
569
+ await mc.ticks(5)
570
+ out = await mc.scoreboard('#match', 'out')
571
+ expect(out).toBe(-1)
572
+ console.log(` match(99) → out=${out} (expect -1, default) ✓`)
573
+ })
574
+
575
+ // Scenario H: while loop counts down from 10 to 0
576
+ // Verifies: while loop body executes correct number of iterations
577
+ test('H: while loop counts down 10 steps', async () => {
578
+ if (!serverOnline) return
579
+
580
+ await mc.command('/function while_test:__load')
581
+ await mc.command('/function while_test:countdown')
582
+ await mc.ticks(10)
583
+
584
+ const i = await mc.scoreboard('#wloop', 'i')
585
+ const steps = await mc.scoreboard('#wloop', 'steps')
586
+ expect(i).toBe(0)
587
+ expect(steps).toBe(10)
588
+ console.log(` while countdown: i=${i} (expect 0), steps=${steps} (expect 10) ✓`)
589
+ })
590
+
591
+ // Scenario I: nested if/else boundary classification
592
+ // Verifies: correct branch taken at boundaries (0, 50, 100)
593
+ test('I: nested if/else boundary classification', async () => {
594
+ if (!serverOnline) return
595
+
596
+ await mc.command('/function boundary_test:__load')
597
+
598
+ // Test x=0 → tier 0
599
+ await mc.command('/scoreboard players set #boundary input 0')
600
+ await mc.command('/function boundary_test:classify_score')
601
+ await mc.ticks(5)
602
+ let tier = await mc.scoreboard('#boundary', 'tier')
603
+ expect(tier).toBe(0)
604
+ console.log(` classify(0) → tier=${tier} (expect 0) ✓`)
605
+
606
+ // Test x=50 → tier 1 (> 0 but not > 50)
607
+ await mc.command('/scoreboard players set #boundary input 50')
608
+ await mc.command('/function boundary_test:classify_score')
609
+ await mc.ticks(5)
610
+ tier = await mc.scoreboard('#boundary', 'tier')
611
+ expect(tier).toBe(1)
612
+ console.log(` classify(50) → tier=${tier} (expect 1) ✓`)
613
+
614
+ // Test x=51 → tier 2 (> 50 but not > 100)
615
+ await mc.command('/scoreboard players set #boundary input 51')
616
+ await mc.command('/function boundary_test:classify_score')
617
+ await mc.ticks(5)
618
+ tier = await mc.scoreboard('#boundary', 'tier')
619
+ expect(tier).toBe(2)
620
+ console.log(` classify(51) → tier=${tier} (expect 2) ✓`)
621
+
622
+ // Test x=101 → tier 3
623
+ await mc.command('/scoreboard players set #boundary input 101')
624
+ await mc.command('/function boundary_test:classify_score')
625
+ await mc.ticks(5)
626
+ tier = await mc.scoreboard('#boundary', 'tier')
627
+ expect(tier).toBe(3)
628
+ console.log(` classify(101) → tier=${tier} (expect 3) ✓`)
629
+ })
630
+
631
+ // Scenario J: entity summon and query
632
+ // Verifies: entities spawned via compiled function are queryable
633
+ test('J: summon entities via compiled function', async () => {
634
+ if (!serverOnline) return
635
+
636
+ await mc.command('/kill @e[type=minecraft:armor_stand]')
637
+ await mc.ticks(2)
638
+ await mc.command('/function tag_test:__load')
639
+ await mc.command('/function tag_test:tag_entities')
640
+ await mc.ticks(5)
641
+
642
+ const stands = await mc.entities('@e[type=minecraft:armor_stand]')
643
+ expect(stands.length).toBe(3)
644
+ console.log(` Summoned 3 armor_stands via tag_test, found: ${stands.length} ✓`)
645
+
646
+ await mc.command('/kill @e[type=minecraft:armor_stand]')
647
+ })
648
+
649
+ // Scenario K: arithmetic order of operations
650
+ // Verifies: MC scoreboard arithmetic matches expected evaluation order
651
+ test('K: arithmetic order of operations', async () => {
652
+ if (!serverOnline) return
653
+
654
+ await mc.command('/function order_test:__load')
655
+ await mc.command('/function order_test:math_order')
656
+ await mc.ticks(10)
657
+
658
+ const r1 = await mc.scoreboard('#order', 'r1')
659
+ const r2 = await mc.scoreboard('#order', 'r2')
660
+ const r3 = await mc.scoreboard('#order', 'r3')
661
+ // a + b * c = 2 + 3*4 = 14 (if precedence respected) or (2+3)*4 = 20 (left-to-right)
662
+ // MC scoreboard does left-to-right, so compiler may emit either depending on lowering
663
+ // (a + b) * c = 5 * 4 = 20 (explicit parens)
664
+ expect(r2).toBe(20) // This one is unambiguous
665
+ // 100 / 3 = 33 (integer division)
666
+ expect(r3).toBe(33)
667
+ console.log(` r1=${r1}, r2=${r2} (expect 20), r3=${r3} (expect 33) ✓`)
668
+ })
669
+
670
+ // Scenario L: scoreboard read-modify-write chain (1 → 2 → 4 → 8)
671
+ // Verifies: sequential RMW operations don't lose intermediate state
672
+ test('L: scoreboard RMW chain — 1*2*2*2 = 8', async () => {
673
+ if (!serverOnline) return
674
+
675
+ await mc.command('/function rmw_test:__load')
676
+ await mc.command('/function rmw_test:chain_rmw')
677
+ await mc.ticks(10)
678
+
679
+ const v = await mc.scoreboard('#rmw', 'v')
680
+ expect(v).toBe(8)
681
+ console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`)
682
+ })
683
+
409
684
  })
@@ -33,14 +33,14 @@ describe('MC Command Syntax Validation', () => {
33
33
  const validator = new MCCommandValidator(FIXTURE_PATH)
34
34
 
35
35
  test('counter example generates valid MC commands', () => {
36
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.rs'), 'utf-8')
36
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8')
37
37
  const errors = validateSource(validator, src, 'counter')
38
38
  expect(errors).toHaveLength(0)
39
39
  })
40
40
 
41
41
  EXAMPLES.forEach(name => {
42
- test(`${name}.rs generates valid MC commands`, () => {
43
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.rs`), 'utf-8')
42
+ test(`${name}.mcrs generates valid MC commands`, () => {
43
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8')
44
44
  const errors = validateSource(validator, src, name)
45
45
 
46
46
  if (errors.length > 0) {
@@ -40,8 +40,8 @@ describe('NBT codec', () => {
40
40
  })
41
41
 
42
42
  describe('Structure generator', () => {
43
- test('compiles counter.rs to a non-empty structure', () => {
44
- const filePath = 'src/examples/counter.rs'
43
+ test('compiles counter.mcrs to a non-empty structure', () => {
44
+ const filePath = 'src/examples/counter.mcrs'
45
45
  const src = fs.readFileSync(filePath, 'utf-8')
46
46
  const { buffer, blockCount } = compileToStructure(src, 'counter', filePath)
47
47
 
@@ -28,7 +28,7 @@ fn turret_tick() {
28
28
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction')
29
29
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction')
30
30
 
31
- const hoistedRead = 'execute store result score $t0 rs run scoreboard players get config turret_range'
31
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config turret_range'
32
32
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0'
33
33
 
34
34
  expect(parent).toContain(hoistedRead)
@@ -54,7 +54,7 @@ fn read_twice() {
54
54
  const readMatches = fn.match(/scoreboard players get @s coins/g) ?? []
55
55
 
56
56
  expect(readMatches).toHaveLength(1)
57
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs')
57
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
58
58
  })
59
59
 
60
60
  test('reuses duplicate arithmetic sequences', () => {
@@ -74,7 +74,7 @@ fn math() {
74
74
  const addMatches = fn.match(/\+= \$const_2 rs/g) ?? []
75
75
 
76
76
  expect(addMatches).toHaveLength(1)
77
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs')
77
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
78
78
  })
79
79
  })
80
80
 
@@ -29,7 +29,7 @@ function loadExample(name: string): string {
29
29
 
30
30
  describe('MCRuntime behavioral integration', () => {
31
31
  it('runs the counter example and increments the scoreboard across ticks', () => {
32
- const runtime = loadCompiledProgram(loadExample('counter.rs'))
32
+ const runtime = loadCompiledProgram(loadExample('counter.mcrs'))
33
33
 
34
34
  runtime.load()
35
35
  runtime.ticks(5)
package/src/ast/types.ts CHANGED
@@ -68,6 +68,13 @@ export interface SelectorFilter {
68
68
  sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary'
69
69
  nbt?: string
70
70
  gamemode?: string
71
+ // Position filters
72
+ x?: RangeExpr
73
+ y?: RangeExpr
74
+ z?: RangeExpr
75
+ // Rotation filters
76
+ x_rotation?: RangeExpr
77
+ y_rotation?: RangeExpr
71
78
  }
72
79
 
73
80
  export interface EntitySelector {
@@ -146,8 +153,8 @@ export type LiteralExpr =
146
153
  export type ExecuteSubcommand =
147
154
  | { kind: 'as'; selector: EntitySelector }
148
155
  | { kind: 'at'; selector: EntitySelector }
149
- | { kind: 'if_entity'; selector: EntitySelector }
150
- | { kind: 'unless_entity'; selector: EntitySelector }
156
+ | { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
157
+ | { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
151
158
  | { kind: 'in'; dimension: string }
152
159
 
153
160
  export type Stmt =
@@ -173,7 +180,7 @@ export type Block = Stmt[]
173
180
  // ---------------------------------------------------------------------------
174
181
 
175
182
  export interface Decorator {
176
- name: 'tick' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
183
+ name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
177
184
  args?: {
178
185
  rate?: number
179
186
  trigger?: string
@@ -235,12 +242,22 @@ export interface ConstDecl {
235
242
  span?: Span
236
243
  }
237
244
 
245
+ export interface GlobalDecl {
246
+ kind: 'global'
247
+ name: string
248
+ type: TypeNode
249
+ init: Expr
250
+ mutable: boolean // let = true, const = false
251
+ span?: Span
252
+ }
253
+
238
254
  // ---------------------------------------------------------------------------
239
255
  // Program (Top-Level)
240
256
  // ---------------------------------------------------------------------------
241
257
 
242
258
  export interface Program {
243
259
  namespace: string // Inferred from filename or `namespace mypack;`
260
+ globals: GlobalDecl[]
244
261
  declarations: FnDecl[]
245
262
  structs: StructDecl[]
246
263
  enums: EnumDecl[]
package/src/cli.ts CHANGED
@@ -29,13 +29,13 @@ Usage:
29
29
  redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
30
30
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
31
  redscript check <file>
32
- redscript fmt <file.rs> [file2.rs ...]
32
+ redscript fmt <file.mcrs> [file2.mcrs ...]
33
33
  redscript repl
34
34
  redscript version
35
35
 
36
36
  Commands:
37
37
  compile Compile a RedScript file to a Minecraft datapack
38
- watch Watch a directory for .rs file changes, recompile, and hot reload
38
+ watch Watch a directory for .mcrs file changes, recompile, and hot reload
39
39
  check Check a RedScript file for errors without generating output
40
40
  fmt Auto-format RedScript source files
41
41
  repl Start an interactive RedScript REPL
@@ -268,7 +268,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
268
268
  process.exit(1)
269
269
  }
270
270
 
271
- console.log(`👁 Watching ${dir} for .rs file changes...`)
271
+ console.log(`👁 Watching ${dir} for .mcrs file changes...`)
272
272
  console.log(` Output: ${output}`)
273
273
  if (hotReloadUrl) console.log(` Hot reload: ${hotReloadUrl}`)
274
274
  console.log(` Press Ctrl+C to stop\n`)
@@ -276,11 +276,11 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
276
276
  // Debounce timer
277
277
  let debounceTimer: NodeJS.Timeout | null = null
278
278
 
279
- // Compile all .rs files in directory
279
+ // Compile all .mcrs files in directory
280
280
  async function compileAll(): Promise<void> {
281
281
  const files = findRsFiles(dir)
282
282
  if (files.length === 0) {
283
- console.log(`⚠ No .rs files found in ${dir}`)
283
+ console.log(`⚠ No .mcrs files found in ${dir}`)
284
284
  return
285
285
  }
286
286
 
@@ -319,7 +319,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
319
319
  }
320
320
  }
321
321
 
322
- // Find all .rs files recursively
322
+ // Find all .mcrs files recursively
323
323
  function findRsFiles(directory: string): string[] {
324
324
  const results: string[] = []
325
325
  const entries = fs.readdirSync(directory, { withFileTypes: true })
@@ -328,7 +328,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
328
328
  const fullPath = path.join(directory, entry.name)
329
329
  if (entry.isDirectory()) {
330
330
  results.push(...findRsFiles(fullPath))
331
- } else if (entry.isFile() && entry.name.endsWith('.rs')) {
331
+ } else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
332
332
  results.push(fullPath)
333
333
  }
334
334
  }
@@ -341,7 +341,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
341
341
 
342
342
  // Watch for changes
343
343
  fs.watch(dir, { recursive: true }, (eventType, filename) => {
344
- if (filename && filename.endsWith('.rs')) {
344
+ if (filename && filename.endsWith('.mcrs')) {
345
345
  // Debounce rapid changes
346
346
  if (debounceTimer) {
347
347
  clearTimeout(debounceTimer)
@@ -412,9 +412,9 @@ async function main(): Promise<void> {
412
412
 
413
413
  case 'fmt':
414
414
  case 'format': {
415
- const files = args.filter(a => a.endsWith('.rs'))
415
+ const files = args.filter(a => a.endsWith('.mcrs'))
416
416
  if (files.length === 0) {
417
- console.error('Usage: redscript fmt <file.rs> [file2.rs ...]')
417
+ console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]')
418
418
  process.exit(1)
419
419
  }
420
420
  const { format } = require('./formatter')
@@ -11,7 +11,7 @@
11
11
  * Variable mapping:
12
12
  * scoreboard objective: "rs"
13
13
  * fake player: "$<varname>"
14
- * temporaries: "$t0", "$t1", ...
14
+ * temporaries: "$_0", "$_1", ...
15
15
  * return value: "$ret"
16
16
  * parameters: "$p0", "$p1", ...
17
17
  */
@@ -293,7 +293,7 @@ export function generateDatapackWithStats(
293
293
  `scoreboard objectives add ${OBJ} dummy`,
294
294
  ]
295
295
  for (const g of module.globals) {
296
- loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ} 0`)
296
+ loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
297
297
  }
298
298
 
299
299
  // Add trigger objectives
@@ -356,6 +356,13 @@ export function generateDatapackWithStats(
356
356
  }
357
357
  }
358
358
 
359
+ // Call @load functions from __load
360
+ for (const fn of module.functions) {
361
+ if (fn.isLoadInit) {
362
+ loadLines.push(`function ${ns}:${fn.name}`)
363
+ }
364
+ }
365
+
359
366
  // Write __load.mcfunction
360
367
  files.push({
361
368
  path: `data/${ns}/function/__load.mcfunction`,
@@ -89,7 +89,7 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
89
89
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
90
90
  const loadCommands = [
91
91
  `scoreboard objectives add ${OBJ} dummy`,
92
- ...module.globals.map(globalName => `scoreboard players set ${varRef(globalName)} ${OBJ} 0`),
92
+ ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
93
93
  ...Array.from(triggerNames).flatMap(triggerName => [
94
94
  `scoreboard objectives add ${triggerName} trigger`,
95
95
  `scoreboard players enable @a ${triggerName}`,
@@ -99,6 +99,13 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
99
99
  ).map(constSetup),
100
100
  ]
101
101
 
102
+ // Call @load functions from __load
103
+ for (const fn of module.functions) {
104
+ if (fn.isLoadInit) {
105
+ loadCommands.push(`function ${module.namespace}:${fn.name}`)
106
+ }
107
+ }
108
+
102
109
  const sections: Array<{ name: string; commands: IRCommand[]; repeat?: boolean }> = []
103
110
 
104
111
  if (loadCommands.length > 0) {