redscript-mc 1.1.0 → 1.2.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 (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/cli.test.js +138 -0
  3. package/dist/__tests__/codegen.test.js +25 -0
  4. package/dist/__tests__/e2e.test.js +190 -12
  5. package/dist/__tests__/lexer.test.js +12 -2
  6. package/dist/__tests__/lowering.test.js +164 -9
  7. package/dist/__tests__/mc-integration.test.js +145 -51
  8. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  9. package/dist/__tests__/parser.test.js +80 -0
  10. package/dist/__tests__/runtime.test.js +8 -8
  11. package/dist/__tests__/typechecker.test.js +158 -0
  12. package/dist/ast/types.d.ts +20 -1
  13. package/dist/codegen/mcfunction/index.js +30 -1
  14. package/dist/codegen/structure/index.js +25 -0
  15. package/dist/compile.d.ts +10 -0
  16. package/dist/compile.js +36 -5
  17. package/dist/events/types.d.ts +35 -0
  18. package/dist/events/types.js +59 -0
  19. package/dist/index.js +3 -2
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +2 -0
  23. package/dist/lowering/index.d.ts +32 -1
  24. package/dist/lowering/index.js +439 -15
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +79 -10
  27. package/dist/typechecker/index.d.ts +17 -0
  28. package/dist/typechecker/index.js +343 -17
  29. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  30. package/editors/vscode/CHANGELOG.md +9 -0
  31. package/editors/vscode/out/extension.js +1144 -72
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +166 -0
  36. package/src/__tests__/codegen.test.ts +27 -0
  37. package/src/__tests__/e2e.test.ts +201 -12
  38. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  39. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  40. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  41. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  42. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  43. package/src/__tests__/lexer.test.ts +14 -2
  44. package/src/__tests__/lowering.test.ts +178 -9
  45. package/src/__tests__/mc-integration.test.ts +166 -51
  46. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  47. package/src/__tests__/parser.test.ts +91 -5
  48. package/src/__tests__/runtime.test.ts +8 -8
  49. package/src/__tests__/typechecker.test.ts +171 -0
  50. package/src/ast/types.ts +25 -1
  51. package/src/codegen/mcfunction/index.ts +31 -1
  52. package/src/codegen/structure/index.ts +27 -0
  53. package/src/compile.ts +54 -6
  54. package/src/events/types.ts +69 -0
  55. package/src/index.ts +4 -3
  56. package/src/ir/types.ts +4 -0
  57. package/src/lexer/index.ts +3 -1
  58. package/src/lowering/index.ts +528 -16
  59. package/src/parser/index.ts +90 -12
  60. package/src/stdlib/README.md +34 -4
  61. package/src/stdlib/tags.mcrs +951 -0
  62. package/src/stdlib/timer.mcrs +54 -33
  63. package/src/typechecker/index.ts +404 -18
@@ -77,6 +77,41 @@ fn test() -> int {
77
77
  const call = getInstructions(specialized).find(i => i.op === 'call');
78
78
  expect(call.fn).toBe('__lambda_0');
79
79
  });
80
+ it('lowers impl methods to prefixed function names', () => {
81
+ const ir = compile(`
82
+ struct Timer { duration: int }
83
+
84
+ impl Timer {
85
+ fn elapsed(self) -> int {
86
+ return self.duration;
87
+ }
88
+ }
89
+ `);
90
+ expect(getFunction(ir, 'Timer_elapsed')).toBeDefined();
91
+ });
92
+ it('lowers impl instance and static method calls', () => {
93
+ const ir = compile(`
94
+ struct Timer { duration: int }
95
+
96
+ impl Timer {
97
+ fn new(duration: int) -> Timer {
98
+ return { duration: duration };
99
+ }
100
+
101
+ fn elapsed(self) -> int {
102
+ return self.duration;
103
+ }
104
+ }
105
+
106
+ fn test() -> int {
107
+ let timer: Timer = Timer::new(10);
108
+ return timer.elapsed();
109
+ }
110
+ `);
111
+ const fn = getFunction(ir, 'test');
112
+ const calls = getInstructions(fn).filter((instr) => instr.op === 'call');
113
+ expect(calls.map(call => call.fn)).toEqual(['Timer_new', 'Timer_elapsed']);
114
+ });
80
115
  });
81
116
  describe('let statements', () => {
82
117
  it('inlines const values without allocating scoreboard variables', () => {
@@ -204,6 +239,23 @@ fn test() -> int {
204
239
  const fn = getFunction(ir, 'foo');
205
240
  expect(fn.blocks.length).toBeGreaterThanOrEqual(3); // entry, then, else, merge
206
241
  });
242
+ it('lowers entity is-checks to execute if entity type filters', () => {
243
+ const ir = compile(`
244
+ fn scan() {
245
+ foreach (e in @e) {
246
+ if (e is Player) {
247
+ kill(e);
248
+ }
249
+ }
250
+ }
251
+ `);
252
+ const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'));
253
+ const rawCmds = getRawCommands(foreachFn);
254
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'));
255
+ expect(isCheckCmd).toBeDefined();
256
+ const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'));
257
+ expect(getRawCommands(thenFn)).toContain('kill @s');
258
+ });
207
259
  });
208
260
  describe('while statements', () => {
209
261
  it('creates loop structure', () => {
@@ -243,6 +295,31 @@ fn test() -> int {
243
295
  const rawCmds = getRawCommands(fn);
244
296
  expect(rawCmds.some(cmd => cmd.includes('data get storage rs:heap arr'))).toBe(true);
245
297
  });
298
+ it('lowers entity is-checks inside foreach bodies', () => {
299
+ const ir = compile(`
300
+ fn test() {
301
+ foreach (e in @e) {
302
+ if (e is Player) {
303
+ give(@s, "diamond", 1);
304
+ }
305
+ if (e is Zombie) {
306
+ kill(@s);
307
+ }
308
+ }
309
+ }
310
+ `);
311
+ const mainFn = getFunction(ir, 'test');
312
+ const foreachFn = ir.functions.find(f => f.name === 'test/foreach_0');
313
+ const thenFns = ir.functions.filter(f => /^test\/then_/.test(f.name)).sort((a, b) => a.name.localeCompare(b.name));
314
+ const rawCmds = getRawCommands(foreachFn);
315
+ const [playerThenFn, zombieThenFn] = thenFns;
316
+ expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0');
317
+ expect(thenFns).toHaveLength(2);
318
+ expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`);
319
+ expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`);
320
+ expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true);
321
+ expect(getRawCommands(zombieThenFn)).toContain('kill @s');
322
+ });
246
323
  });
247
324
  describe('match statements', () => {
248
325
  it('lowers match into guarded execute function calls', () => {
@@ -515,12 +592,12 @@ fn test() {
515
592
  `);
516
593
  const fn = getFunction(ir, 'test');
517
594
  const rawCmds = getRawCommands(fn);
518
- expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar kills');
519
- expect(rawCmds).toContain('scoreboard objectives setdisplay list coins');
520
- expect(rawCmds).toContain('scoreboard objectives setdisplay belowName hp');
595
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills');
596
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins');
597
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp');
521
598
  expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar');
522
- expect(rawCmds).toContain('scoreboard objectives add kills playerKillCount "Kill Count"');
523
- expect(rawCmds).toContain('scoreboard objectives remove kills');
599
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
600
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills');
524
601
  });
525
602
  it('lowers bossbar management builtins', () => {
526
603
  const ir = compile(`
@@ -588,6 +665,40 @@ fn test() {
588
665
  const rawCmds = getRawCommands(fn);
589
666
  expect(rawCmds).toContain('random reset loot 42');
590
667
  });
668
+ it('lowers setTimeout() to a scheduled helper function', () => {
669
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }');
670
+ const fn = getFunction(ir, 'test');
671
+ const timeoutFn = getFunction(ir, '__timeout_0');
672
+ const rawCmds = getRawCommands(fn);
673
+ const timeoutCmds = getRawCommands(timeoutFn);
674
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
675
+ expect(timeoutCmds).toContain('say hi');
676
+ });
677
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
678
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }');
679
+ const fn = getFunction(ir, 'test');
680
+ const intervalFn = getFunction(ir, '__interval_0');
681
+ const intervalBodyFn = getFunction(ir, '__interval_body_0');
682
+ const rawCmds = getRawCommands(fn);
683
+ const intervalCmds = getRawCommands(intervalFn);
684
+ const intervalBodyCmds = getRawCommands(intervalBodyFn);
685
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
686
+ expect(intervalCmds).toContain('function test:__interval_body_0');
687
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t');
688
+ expect(intervalBodyCmds).toContain('say tick');
689
+ });
690
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
691
+ const ir = compile(`
692
+ fn test() {
693
+ let intervalId: int = setInterval(20, () => { say("tick"); });
694
+ clearInterval(intervalId);
695
+ }
696
+ `);
697
+ const fn = getFunction(ir, 'test');
698
+ const rawCmds = getRawCommands(fn);
699
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
700
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
701
+ });
591
702
  it('lowers data_get from entity', () => {
592
703
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }');
593
704
  const fn = getFunction(ir, 'test');
@@ -628,19 +739,25 @@ fn test() {
628
739
  const ir = compile('fn test() { let score: int = scoreboard_get(@s, "score"); }');
629
740
  const fn = getFunction(ir, 'test');
630
741
  const rawCmds = getRawCommands(fn);
631
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
742
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
632
743
  });
633
744
  it('accepts bare selector targets in scoreboard_set', () => {
634
745
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }');
635
746
  const fn = getFunction(ir, 'test');
636
747
  const rawCmds = getRawCommands(fn);
637
- expect(rawCmds).toContain('scoreboard players set @a kills 0');
748
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0');
749
+ });
750
+ it('skips prefixing raw mc_name objectives', () => {
751
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }');
752
+ const fn = getFunction(ir, 'test');
753
+ const rawCmds = getRawCommands(fn);
754
+ expect(rawCmds).toContain('scoreboard players set @s health 100');
638
755
  });
639
756
  it('warns on quoted selectors in scoreboard_get', () => {
640
757
  const { ir, warnings } = compileWithWarnings('fn test() { let score: int = scoreboard_get("@s", "score"); }');
641
758
  const fn = getFunction(ir, 'test');
642
759
  const rawCmds = getRawCommands(fn);
643
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
760
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
644
761
  expect(warnings).toContainEqual(expect.objectContaining({
645
762
  code: 'W_QUOTED_SELECTOR',
646
763
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
@@ -650,7 +767,7 @@ fn test() {
650
767
  const { ir, warnings } = compileWithWarnings('fn test() { let total: int = scoreboard_get("#global", "total"); }');
651
768
  const fn = getFunction(ir, 'test');
652
769
  const rawCmds = getRawCommands(fn);
653
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global total'))).toBe(true);
770
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global test.total'))).toBe(true);
654
771
  expect(warnings).toHaveLength(0);
655
772
  });
656
773
  it('warns on quoted selectors in data_get entity targets', () => {
@@ -663,6 +780,37 @@ fn test() {
663
780
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
664
781
  }));
665
782
  });
783
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
784
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }');
785
+ const fn = getFunction(ir, 'test');
786
+ const rawCmds = getRawCommands(fn);
787
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5');
788
+ });
789
+ });
790
+ describe('timer builtins', () => {
791
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
792
+ const ir = compile(`
793
+ fn test() {
794
+ let intervalId: int = setInterval(20, () => {
795
+ say("tick");
796
+ });
797
+ setTimeout(100, () => {
798
+ say("later");
799
+ });
800
+ clearInterval(intervalId);
801
+ }
802
+ `);
803
+ const fn = getFunction(ir, 'test');
804
+ const rawCmds = getRawCommands(fn);
805
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
806
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
807
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
808
+ const intervalFn = getFunction(ir, '__interval_0');
809
+ expect(getRawCommands(intervalFn)).toEqual([
810
+ 'function test:__interval_body_0',
811
+ 'schedule function test:__interval_0 20t',
812
+ ]);
813
+ });
666
814
  });
667
815
  describe('decorators', () => {
668
816
  it('marks @tick function', () => {
@@ -681,6 +829,13 @@ fn test() {
681
829
  const fn = getFunction(ir, 'handle_advancement');
682
830
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' });
683
831
  });
832
+ it('marks @on event functions and binds player to @s', () => {
833
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }');
834
+ const fn = getFunction(ir, 'handle_death');
835
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' });
836
+ expect(fn.params).toEqual([]);
837
+ expect(getRawCommands(fn)).toContain('tp @s @p');
838
+ });
684
839
  });
685
840
  describe('selectors', () => {
686
841
  it('converts selector with filters to string', () => {
@@ -52,6 +52,7 @@ const MC_HOST = process.env.MC_HOST ?? 'localhost';
52
52
  const MC_PORT = parseInt(process.env.MC_PORT ?? '25561');
53
53
  const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME, 'mc-test-server');
54
54
  const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test');
55
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures');
55
56
  let serverOnline = false;
56
57
  let mc;
57
58
  /** Write compiled RedScript source into the shared test datapack directory.
@@ -84,9 +85,22 @@ function writeFixture(source, namespace) {
84
85
  }
85
86
  }
86
87
  }
88
+ function writeFixtureFile(fileName, namespace) {
89
+ writeFixture(fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf-8'), namespace);
90
+ }
91
+ async function waitForServer(client, timeoutMs = 30000) {
92
+ const deadline = Date.now() + timeoutMs;
93
+ while (Date.now() < deadline) {
94
+ if (await client.isOnline()) {
95
+ return true;
96
+ }
97
+ await new Promise(resolve => setTimeout(resolve, 1000));
98
+ }
99
+ return false;
100
+ }
87
101
  beforeAll(async () => {
88
102
  mc = new client_1.MCTestClient(MC_HOST, MC_PORT);
89
- serverOnline = await mc.isOnline();
103
+ serverOnline = await waitForServer(mc);
90
104
  if (!serverOnline) {
91
105
  console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`);
92
106
  console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`);
@@ -104,16 +118,16 @@ beforeAll(async () => {
104
118
  writeFixture(`
105
119
  @tick
106
120
  fn on_tick() {
107
- scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
121
+ scoreboard_set("#tick_counter", #ticks, scoreboard_get("#tick_counter", #ticks) + 1);
108
122
  }
109
123
  `, 'tick_test');
110
124
  writeFixture(`
111
125
  fn check_score() {
112
- let x: int = scoreboard_get("#check_x", "test_score");
126
+ let x: int = scoreboard_get("#check_x", #test_score);
113
127
  if (x > 5) {
114
- scoreboard_set("#check_x", "result", 1);
128
+ scoreboard_set("#check_x", #result, 1);
115
129
  } else {
116
- scoreboard_set("#check_x", "result", 0);
130
+ scoreboard_set("#check_x", #result, 0);
117
131
  }
118
132
  }
119
133
  `, 'inline_test');
@@ -122,30 +136,30 @@ beforeAll(async () => {
122
136
  writeFixture(`
123
137
  @tick
124
138
  fn game_tick() {
125
- let time: int = scoreboard_get("#game", "timer");
139
+ let time: int = scoreboard_get("#game", #timer);
126
140
  if (time > 0) {
127
- scoreboard_set("#game", "timer", time - 1);
141
+ scoreboard_set("#game", #timer, time - 1);
128
142
  }
129
143
  if (time == 1) {
130
- scoreboard_set("#game", "ended", 1);
144
+ scoreboard_set("#game", #ended, 1);
131
145
  }
132
146
  }
133
147
  fn start_game() {
134
- scoreboard_set("#game", "timer", 5);
135
- scoreboard_set("#game", "ended", 0);
148
+ scoreboard_set("#game", #timer, 5);
149
+ scoreboard_set("#game", #ended, 0);
136
150
  }
137
151
  `, 'game_loop');
138
152
  // Scenario B: two functions, same temp var namespace — verify no collision
139
153
  writeFixture(`
140
154
  fn calc_sum() {
141
- let a: int = scoreboard_get("#math", "val_a");
142
- let b: int = scoreboard_get("#math", "val_b");
143
- scoreboard_set("#math", "sum", a + b);
155
+ let a: int = scoreboard_get("#math", #val_a);
156
+ let b: int = scoreboard_get("#math", #val_b);
157
+ scoreboard_set("#math", #sum, a + b);
144
158
  }
145
159
  fn calc_product() {
146
- let x: int = scoreboard_get("#math", "val_x");
147
- let y: int = scoreboard_get("#math", "val_y");
148
- scoreboard_set("#math", "product", x * y);
160
+ let x: int = scoreboard_get("#math", #val_x);
161
+ let y: int = scoreboard_get("#math", #val_y);
162
+ scoreboard_set("#math", #product, x * y);
149
163
  }
150
164
  fn run_both() {
151
165
  calc_sum();
@@ -155,16 +169,16 @@ beforeAll(async () => {
155
169
  // Scenario C: 3-deep call chain, each step modifies shared state
156
170
  writeFixture(`
157
171
  fn step3() {
158
- let v: int = scoreboard_get("#chain", "val");
159
- scoreboard_set("#chain", "val", v * 2);
172
+ let v: int = scoreboard_get("#chain", #val);
173
+ scoreboard_set("#chain", #val, v * 2);
160
174
  }
161
175
  fn step2() {
162
- let v: int = scoreboard_get("#chain", "val");
163
- scoreboard_set("#chain", "val", v + 5);
176
+ let v: int = scoreboard_get("#chain", #val);
177
+ scoreboard_set("#chain", #val, v + 5);
164
178
  step3();
165
179
  }
166
180
  fn step1() {
167
- scoreboard_set("#chain", "val", 10);
181
+ scoreboard_set("#chain", #val, 10);
168
182
  step2();
169
183
  }
170
184
  `, 'call_chain');
@@ -180,10 +194,10 @@ beforeAll(async () => {
180
194
  // Scenario E: for-range loop — loop counter increments exactly N times
181
195
  writeFixture(`
182
196
  fn count_to_five() {
183
- scoreboard_set("#range", "counter", 0);
197
+ scoreboard_set("#range", #counter, 0);
184
198
  for i in 0..5 {
185
- let c: int = scoreboard_get("#range", "counter");
186
- scoreboard_set("#range", "counter", c + 1);
199
+ let c: int = scoreboard_get("#range", #counter);
200
+ scoreboard_set("#range", #counter, c + 1);
187
201
  }
188
202
  }
189
203
  `, 'range_test');
@@ -194,48 +208,48 @@ beforeAll(async () => {
194
208
  }
195
209
  fn run_nested() {
196
210
  let a: int = triple(4);
197
- scoreboard_set("#nested", "result", a);
211
+ scoreboard_set("#nested", #result, a);
198
212
  }
199
213
  `, 'nested_test');
200
214
  // Scenario G: match statement dispatches to correct branch
201
215
  writeFixture(`
202
216
  fn classify(x: int) {
203
217
  match (x) {
204
- 1 => { scoreboard_set("#match", "out", 10); }
205
- 2 => { scoreboard_set("#match", "out", 20); }
206
- 3 => { scoreboard_set("#match", "out", 30); }
207
- _ => { scoreboard_set("#match", "out", -1); }
218
+ 1 => { scoreboard_set("#match", #out, 10); }
219
+ 2 => { scoreboard_set("#match", #out, 20); }
220
+ 3 => { scoreboard_set("#match", #out, 30); }
221
+ _ => { scoreboard_set("#match", #out, -1); }
208
222
  }
209
223
  }
210
224
  `, 'match_test');
211
225
  // Scenario H: while loop counts down
212
226
  writeFixture(`
213
227
  fn countdown() {
214
- scoreboard_set("#wloop", "i", 10);
215
- scoreboard_set("#wloop", "steps", 0);
216
- let i: int = scoreboard_get("#wloop", "i");
228
+ scoreboard_set("#wloop", #i, 10);
229
+ scoreboard_set("#wloop", #steps, 0);
230
+ let i: int = scoreboard_get("#wloop", #i);
217
231
  while (i > 0) {
218
- let s: int = scoreboard_get("#wloop", "steps");
219
- scoreboard_set("#wloop", "steps", s + 1);
232
+ let s: int = scoreboard_get("#wloop", #steps);
233
+ scoreboard_set("#wloop", #steps, s + 1);
220
234
  i = i - 1;
221
- scoreboard_set("#wloop", "i", i);
235
+ scoreboard_set("#wloop", #i, i);
222
236
  }
223
237
  }
224
238
  `, 'while_test');
225
239
  // Scenario I: multiple if/else branches (boundary test)
226
240
  writeFixture(`
227
241
  fn classify_score() {
228
- let x: int = scoreboard_get("#boundary", "input");
242
+ let x: int = scoreboard_get("#boundary", #input);
229
243
  if (x > 100) {
230
- scoreboard_set("#boundary", "tier", 3);
244
+ scoreboard_set("#boundary", #tier, 3);
231
245
  } else {
232
246
  if (x > 50) {
233
- scoreboard_set("#boundary", "tier", 2);
247
+ scoreboard_set("#boundary", #tier, 2);
234
248
  } else {
235
249
  if (x > 0) {
236
- scoreboard_set("#boundary", "tier", 1);
250
+ scoreboard_set("#boundary", #tier, 1);
237
251
  } else {
238
- scoreboard_set("#boundary", "tier", 0);
252
+ scoreboard_set("#boundary", #tier, 0);
239
253
  }
240
254
  }
241
255
  }
@@ -255,31 +269,37 @@ beforeAll(async () => {
255
269
  let a: int = 2;
256
270
  let b: int = 3;
257
271
  let c: int = 4;
258
- scoreboard_set("#order", "r1", a + b * c);
259
- scoreboard_set("#order", "r2", (a + b) * c);
272
+ scoreboard_set("#order", #r1, a + b * c);
273
+ scoreboard_set("#order", #r2, (a + b) * c);
260
274
  let d: int = 100;
261
275
  let e: int = d / 3;
262
- scoreboard_set("#order", "r3", e);
276
+ scoreboard_set("#order", #r3, e);
263
277
  }
264
278
  `, 'order_test');
265
279
  // Scenario L: scoreboard read-modify-write chain
266
280
  writeFixture(`
267
281
  fn chain_rmw() {
268
- scoreboard_set("#rmw", "v", 1);
269
- let v: int = scoreboard_get("#rmw", "v");
270
- scoreboard_set("#rmw", "v", v * 2);
271
- v = scoreboard_get("#rmw", "v");
272
- scoreboard_set("#rmw", "v", v * 2);
273
- v = scoreboard_get("#rmw", "v");
274
- scoreboard_set("#rmw", "v", v * 2);
282
+ scoreboard_set("#rmw", #v, 1);
283
+ let v: int = scoreboard_get("#rmw", #v);
284
+ scoreboard_set("#rmw", #v, v * 2);
285
+ v = scoreboard_get("#rmw", #v);
286
+ scoreboard_set("#rmw", #v, v * 2);
287
+ v = scoreboard_get("#rmw", #v);
288
+ scoreboard_set("#rmw", #v, v * 2);
275
289
  }
276
290
  `, 'rmw_test');
291
+ writeFixtureFile('impl-test.mcrs', 'impl_test');
292
+ writeFixtureFile('timeout-test.mcrs', 'timeout_test');
293
+ writeFixtureFile('interval-test.mcrs', 'interval_test');
294
+ writeFixtureFile('is-check-test.mcrs', 'is_check_test');
295
+ writeFixtureFile('event-test.mcrs', 'event_test');
277
296
  // ── Full reset + safe data reload ────────────────────────────────────
278
297
  await mc.fullReset();
279
298
  // Pre-create scoreboards
280
299
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
281
300
  'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
282
- 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v']) {
301
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v',
302
+ 'done', 'fired', 'players', 'zombies']) {
283
303
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => { });
284
304
  }
285
305
  await mc.command('/scoreboard players set counter ticks 0');
@@ -637,4 +657,78 @@ describe('E2E Scenario Tests', () => {
637
657
  console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`);
638
658
  });
639
659
  });
660
+ describe('MC Integration - New Features', () => {
661
+ test('impl-test.mcrs: Timer::new/start/tick/done works in-game', async () => {
662
+ if (!serverOnline)
663
+ return;
664
+ await mc.command('/scoreboard players set #impl done 0');
665
+ await mc.command('/scoreboard players set timer_ticks rs 0');
666
+ await mc.command('/scoreboard players set timer_active rs 0');
667
+ await mc.command('/function impl_test:__load').catch(() => { });
668
+ await mc.command('/function impl_test:test');
669
+ await mc.ticks(5);
670
+ const done = await mc.scoreboard('#impl', 'done');
671
+ const ticks = await mc.scoreboard('timer_ticks', 'rs');
672
+ expect(done).toBe(1);
673
+ expect(ticks).toBe(3);
674
+ });
675
+ test('timeout-test.mcrs: setTimeout executes after delay', async () => {
676
+ if (!serverOnline)
677
+ return;
678
+ await mc.command('/scoreboard players set #timeout fired 0');
679
+ await mc.command('/function timeout_test:__load').catch(() => { });
680
+ await mc.command('/function timeout_test:start');
681
+ await mc.ticks(10);
682
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(0);
683
+ await mc.ticks(15);
684
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(1);
685
+ });
686
+ test('interval-test.mcrs: setInterval repeats on schedule', async () => {
687
+ if (!serverOnline)
688
+ return;
689
+ await mc.command('/scoreboard players set #interval ticks 0');
690
+ await mc.command('/function interval_test:__load').catch(() => { });
691
+ await mc.command('/function interval_test:start');
692
+ await mc.ticks(70);
693
+ const count = await mc.scoreboard('#interval', 'ticks');
694
+ expect(count).toBeGreaterThanOrEqual(3);
695
+ expect(count).toBeLessThanOrEqual(3);
696
+ });
697
+ test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
698
+ if (!serverOnline)
699
+ return;
700
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
701
+ await mc.command('/scoreboard players set #is_check players 0');
702
+ await mc.command('/scoreboard players set #is_check zombies 0');
703
+ await mc.command('/function is_check_test:__load').catch(() => { });
704
+ await mc.command('/summon minecraft:zombie 0 65 0');
705
+ await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target');
706
+ await mc.command('/summon minecraft:armor_stand 2 65 0');
707
+ await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target');
708
+ await mc.command('/function is_check_test:check_types');
709
+ await mc.ticks(5);
710
+ const zombies = await mc.scoreboard('#is_check', 'zombies');
711
+ const players = await mc.scoreboard('#is_check', 'players');
712
+ const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]');
713
+ const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]');
714
+ expect(zombies).toBe(1);
715
+ expect(players).toBe(0);
716
+ expect(zombieEntities).toHaveLength(0);
717
+ expect(standEntities).toHaveLength(1);
718
+ await mc.command('/kill @e[tag=is_check_target]').catch(() => { });
719
+ });
720
+ test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
721
+ if (!serverOnline)
722
+ return;
723
+ // Verify the event system compiles correctly
724
+ await mc.command('/function event_test:__load').catch(() => { });
725
+ await mc.ticks(5);
726
+ // Verify the trigger function exists
727
+ const result = await mc.command('/function event_test:trigger_fake_death');
728
+ expect(result.ok).toBe(true);
729
+ // Verify __tick exists (event dispatcher)
730
+ const tickResult = await mc.command('/function event_test:__tick').catch(() => ({ ok: false }));
731
+ expect(tickResult.ok).toBe(true);
732
+ });
733
+ });
640
734
  //# sourceMappingURL=mc-integration.test.js.map
@@ -26,11 +26,11 @@ fn turret_tick() {
26
26
  const result = (0, index_1.compile)(source, { namespace: 'test' });
27
27
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction');
28
28
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction');
29
- const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config turret_range';
29
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.turret_range';
30
30
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0';
31
31
  expect(parent).toContain(hoistedRead);
32
32
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall));
33
- expect(loopBody).not.toContain('scoreboard players get config turret_range');
33
+ expect(loopBody).not.toContain('scoreboard players get config test.turret_range');
34
34
  });
35
35
  });
36
36
  describe('CSE', () => {
@@ -46,7 +46,7 @@ fn read_twice() {
46
46
  `;
47
47
  const result = (0, index_1.compile)(source, { namespace: 'test' });
48
48
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction');
49
- const readMatches = fn.match(/scoreboard players get @s coins/g) ?? [];
49
+ const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? [];
50
50
  expect(readMatches).toHaveLength(1);
51
51
  expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
52
52
  });