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
@@ -88,6 +88,43 @@ fn test() -> int {
88
88
  const call = getInstructions(specialized!).find(i => i.op === 'call') as any
89
89
  expect(call.fn).toBe('__lambda_0')
90
90
  })
91
+
92
+ it('lowers impl methods to prefixed function names', () => {
93
+ const ir = compile(`
94
+ struct Timer { duration: int }
95
+
96
+ impl Timer {
97
+ fn elapsed(self) -> int {
98
+ return self.duration;
99
+ }
100
+ }
101
+ `)
102
+ expect(getFunction(ir, 'Timer_elapsed')).toBeDefined()
103
+ })
104
+
105
+ it('lowers impl instance and static method calls', () => {
106
+ const ir = compile(`
107
+ struct Timer { duration: int }
108
+
109
+ impl Timer {
110
+ fn new(duration: int) -> Timer {
111
+ return { duration: duration };
112
+ }
113
+
114
+ fn elapsed(self) -> int {
115
+ return self.duration;
116
+ }
117
+ }
118
+
119
+ fn test() -> int {
120
+ let timer: Timer = Timer::new(10);
121
+ return timer.elapsed();
122
+ }
123
+ `)
124
+ const fn = getFunction(ir, 'test')!
125
+ const calls = getInstructions(fn).filter((instr): instr is IRInstr & { op: 'call' } => instr.op === 'call')
126
+ expect(calls.map(call => call.fn)).toEqual(['Timer_new', 'Timer_elapsed'])
127
+ })
91
128
  })
92
129
 
93
130
  describe('let statements', () => {
@@ -235,6 +272,25 @@ fn test() -> int {
235
272
  const fn = getFunction(ir, 'foo')!
236
273
  expect(fn.blocks.length).toBeGreaterThanOrEqual(3) // entry, then, else, merge
237
274
  })
275
+
276
+ it('lowers entity is-checks to execute if entity type filters', () => {
277
+ const ir = compile(`
278
+ fn scan() {
279
+ foreach (e in @e) {
280
+ if (e is Player) {
281
+ kill(e);
282
+ }
283
+ }
284
+ }
285
+ `)
286
+ const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'))!
287
+ const rawCmds = getRawCommands(foreachFn)
288
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'))
289
+ expect(isCheckCmd).toBeDefined()
290
+
291
+ const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'))!
292
+ expect(getRawCommands(thenFn)).toContain('kill @s')
293
+ })
238
294
  })
239
295
 
240
296
  describe('while statements', () => {
@@ -282,6 +338,33 @@ fn test() -> int {
282
338
  const rawCmds = getRawCommands(fn)
283
339
  expect(rawCmds.some(cmd => cmd.includes('data get storage rs:heap arr'))).toBe(true)
284
340
  })
341
+
342
+ it('lowers entity is-checks inside foreach bodies', () => {
343
+ const ir = compile(`
344
+ fn test() {
345
+ foreach (e in @e) {
346
+ if (e is Player) {
347
+ give(@s, "diamond", 1);
348
+ }
349
+ if (e is Zombie) {
350
+ kill(@s);
351
+ }
352
+ }
353
+ }
354
+ `)
355
+ const mainFn = getFunction(ir, 'test')!
356
+ const foreachFn = ir.functions.find(f => f.name === 'test/foreach_0')!
357
+ const thenFns = ir.functions.filter(f => /^test\/then_/.test(f.name)).sort((a, b) => a.name.localeCompare(b.name))
358
+ const rawCmds = getRawCommands(foreachFn)
359
+ const [playerThenFn, zombieThenFn] = thenFns
360
+
361
+ expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0')
362
+ expect(thenFns).toHaveLength(2)
363
+ expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`)
364
+ expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`)
365
+ expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true)
366
+ expect(getRawCommands(zombieThenFn)).toContain('kill @s')
367
+ })
285
368
  })
286
369
 
287
370
  describe('match statements', () => {
@@ -603,12 +686,12 @@ fn test() {
603
686
  `)
604
687
  const fn = getFunction(ir, 'test')!
605
688
  const rawCmds = getRawCommands(fn)
606
- expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar kills')
607
- expect(rawCmds).toContain('scoreboard objectives setdisplay list coins')
608
- expect(rawCmds).toContain('scoreboard objectives setdisplay belowName hp')
689
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills')
690
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins')
691
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp')
609
692
  expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar')
610
- expect(rawCmds).toContain('scoreboard objectives add kills playerKillCount "Kill Count"')
611
- expect(rawCmds).toContain('scoreboard objectives remove kills')
693
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"')
694
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills')
612
695
  })
613
696
 
614
697
  it('lowers bossbar management builtins', () => {
@@ -682,6 +765,43 @@ fn test() {
682
765
  expect(rawCmds).toContain('random reset loot 42')
683
766
  })
684
767
 
768
+ it('lowers setTimeout() to a scheduled helper function', () => {
769
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }')
770
+ const fn = getFunction(ir, 'test')!
771
+ const timeoutFn = getFunction(ir, '__timeout_0')!
772
+ const rawCmds = getRawCommands(fn)
773
+ const timeoutCmds = getRawCommands(timeoutFn)
774
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
775
+ expect(timeoutCmds).toContain('say hi')
776
+ })
777
+
778
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
779
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }')
780
+ const fn = getFunction(ir, 'test')!
781
+ const intervalFn = getFunction(ir, '__interval_0')!
782
+ const intervalBodyFn = getFunction(ir, '__interval_body_0')!
783
+ const rawCmds = getRawCommands(fn)
784
+ const intervalCmds = getRawCommands(intervalFn)
785
+ const intervalBodyCmds = getRawCommands(intervalBodyFn)
786
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
787
+ expect(intervalCmds).toContain('function test:__interval_body_0')
788
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t')
789
+ expect(intervalBodyCmds).toContain('say tick')
790
+ })
791
+
792
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
793
+ const ir = compile(`
794
+ fn test() {
795
+ let intervalId: int = setInterval(20, () => { say("tick"); });
796
+ clearInterval(intervalId);
797
+ }
798
+ `)
799
+ const fn = getFunction(ir, 'test')!
800
+ const rawCmds = getRawCommands(fn)
801
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
802
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
803
+ })
804
+
685
805
  it('lowers data_get from entity', () => {
686
806
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }')
687
807
  const fn = getFunction(ir, 'test')!
@@ -736,7 +856,7 @@ fn test() {
736
856
  const fn = getFunction(ir, 'test')!
737
857
  const rawCmds = getRawCommands(fn)
738
858
  expect(rawCmds.some(cmd =>
739
- cmd.includes('run scoreboard players get @s score')
859
+ cmd.includes('run scoreboard players get @s test.score')
740
860
  )).toBe(true)
741
861
  })
742
862
 
@@ -744,7 +864,14 @@ fn test() {
744
864
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }')
745
865
  const fn = getFunction(ir, 'test')!
746
866
  const rawCmds = getRawCommands(fn)
747
- expect(rawCmds).toContain('scoreboard players set @a kills 0')
867
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0')
868
+ })
869
+
870
+ it('skips prefixing raw mc_name objectives', () => {
871
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }')
872
+ const fn = getFunction(ir, 'test')!
873
+ const rawCmds = getRawCommands(fn)
874
+ expect(rawCmds).toContain('scoreboard players set @s health 100')
748
875
  })
749
876
 
750
877
  it('warns on quoted selectors in scoreboard_get', () => {
@@ -752,7 +879,7 @@ fn test() {
752
879
  const fn = getFunction(ir, 'test')!
753
880
  const rawCmds = getRawCommands(fn)
754
881
  expect(rawCmds.some(cmd =>
755
- cmd.includes('run scoreboard players get @s score')
882
+ cmd.includes('run scoreboard players get @s test.score')
756
883
  )).toBe(true)
757
884
  expect(warnings).toContainEqual(expect.objectContaining({
758
885
  code: 'W_QUOTED_SELECTOR',
@@ -765,7 +892,7 @@ fn test() {
765
892
  const fn = getFunction(ir, 'test')!
766
893
  const rawCmds = getRawCommands(fn)
767
894
  expect(rawCmds.some(cmd =>
768
- cmd.includes('run scoreboard players get #global total')
895
+ cmd.includes('run scoreboard players get #global test.total')
769
896
  )).toBe(true)
770
897
  expect(warnings).toHaveLength(0)
771
898
  })
@@ -782,6 +909,40 @@ fn test() {
782
909
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
783
910
  }))
784
911
  })
912
+
913
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
914
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }')
915
+ const fn = getFunction(ir, 'test')!
916
+ const rawCmds = getRawCommands(fn)
917
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5')
918
+ })
919
+ })
920
+
921
+ describe('timer builtins', () => {
922
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
923
+ const ir = compile(`
924
+ fn test() {
925
+ let intervalId: int = setInterval(20, () => {
926
+ say("tick");
927
+ });
928
+ setTimeout(100, () => {
929
+ say("later");
930
+ });
931
+ clearInterval(intervalId);
932
+ }
933
+ `)
934
+ const fn = getFunction(ir, 'test')!
935
+ const rawCmds = getRawCommands(fn)
936
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
937
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
938
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
939
+
940
+ const intervalFn = getFunction(ir, '__interval_0')!
941
+ expect(getRawCommands(intervalFn)).toEqual([
942
+ 'function test:__interval_body_0',
943
+ 'schedule function test:__interval_0 20t',
944
+ ])
945
+ })
785
946
  })
786
947
 
787
948
  describe('decorators', () => {
@@ -803,6 +964,14 @@ fn test() {
803
964
  const fn = getFunction(ir, 'handle_advancement')!
804
965
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' })
805
966
  })
967
+
968
+ it('marks @on event functions and binds player to @s', () => {
969
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }')
970
+ const fn = getFunction(ir, 'handle_death')!
971
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' })
972
+ expect(fn.params).toEqual([])
973
+ expect(getRawCommands(fn)).toContain('tp @s @p')
974
+ })
806
975
  })
807
976
 
808
977
  describe('selectors', () => {
@@ -19,6 +19,7 @@ const MC_HOST = process.env.MC_HOST ?? 'localhost'
19
19
  const MC_PORT = parseInt(process.env.MC_PORT ?? '25561')
20
20
  const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME!, 'mc-test-server')
21
21
  const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test')
22
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures')
22
23
 
23
24
  let serverOnline = false
24
25
  let mc: MCTestClient
@@ -54,9 +55,27 @@ function writeFixture(source: string, namespace: string): void {
54
55
  }
55
56
  }
56
57
 
58
+ function writeFixtureFile(fileName: string, namespace: string): void {
59
+ writeFixture(
60
+ fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf-8'),
61
+ namespace
62
+ )
63
+ }
64
+
65
+ async function waitForServer(client: MCTestClient, timeoutMs = 30000): Promise<boolean> {
66
+ const deadline = Date.now() + timeoutMs
67
+ while (Date.now() < deadline) {
68
+ if (await client.isOnline()) {
69
+ return true
70
+ }
71
+ await new Promise(resolve => setTimeout(resolve, 1000))
72
+ }
73
+ return false
74
+ }
75
+
57
76
  beforeAll(async () => {
58
77
  mc = new MCTestClient(MC_HOST, MC_PORT)
59
- serverOnline = await mc.isOnline()
78
+ serverOnline = await waitForServer(mc)
60
79
  if (!serverOnline) {
61
80
  console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`)
62
81
  console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`)
@@ -75,16 +94,16 @@ beforeAll(async () => {
75
94
  writeFixture(`
76
95
  @tick
77
96
  fn on_tick() {
78
- scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
97
+ scoreboard_set("#tick_counter", #ticks, scoreboard_get("#tick_counter", #ticks) + 1);
79
98
  }
80
99
  `, 'tick_test')
81
100
  writeFixture(`
82
101
  fn check_score() {
83
- let x: int = scoreboard_get("#check_x", "test_score");
102
+ let x: int = scoreboard_get("#check_x", #test_score);
84
103
  if (x > 5) {
85
- scoreboard_set("#check_x", "result", 1);
104
+ scoreboard_set("#check_x", #result, 1);
86
105
  } else {
87
- scoreboard_set("#check_x", "result", 0);
106
+ scoreboard_set("#check_x", #result, 0);
88
107
  }
89
108
  }
90
109
  `, 'inline_test')
@@ -95,31 +114,31 @@ beforeAll(async () => {
95
114
  writeFixture(`
96
115
  @tick
97
116
  fn game_tick() {
98
- let time: int = scoreboard_get("#game", "timer");
117
+ let time: int = scoreboard_get("#game", #timer);
99
118
  if (time > 0) {
100
- scoreboard_set("#game", "timer", time - 1);
119
+ scoreboard_set("#game", #timer, time - 1);
101
120
  }
102
121
  if (time == 1) {
103
- scoreboard_set("#game", "ended", 1);
122
+ scoreboard_set("#game", #ended, 1);
104
123
  }
105
124
  }
106
125
  fn start_game() {
107
- scoreboard_set("#game", "timer", 5);
108
- scoreboard_set("#game", "ended", 0);
126
+ scoreboard_set("#game", #timer, 5);
127
+ scoreboard_set("#game", #ended, 0);
109
128
  }
110
129
  `, 'game_loop')
111
130
 
112
131
  // Scenario B: two functions, same temp var namespace — verify no collision
113
132
  writeFixture(`
114
133
  fn calc_sum() {
115
- let a: int = scoreboard_get("#math", "val_a");
116
- let b: int = scoreboard_get("#math", "val_b");
117
- scoreboard_set("#math", "sum", a + b);
134
+ let a: int = scoreboard_get("#math", #val_a);
135
+ let b: int = scoreboard_get("#math", #val_b);
136
+ scoreboard_set("#math", #sum, a + b);
118
137
  }
119
138
  fn calc_product() {
120
- let x: int = scoreboard_get("#math", "val_x");
121
- let y: int = scoreboard_get("#math", "val_y");
122
- scoreboard_set("#math", "product", x * y);
139
+ let x: int = scoreboard_get("#math", #val_x);
140
+ let y: int = scoreboard_get("#math", #val_y);
141
+ scoreboard_set("#math", #product, x * y);
123
142
  }
124
143
  fn run_both() {
125
144
  calc_sum();
@@ -130,16 +149,16 @@ beforeAll(async () => {
130
149
  // Scenario C: 3-deep call chain, each step modifies shared state
131
150
  writeFixture(`
132
151
  fn step3() {
133
- let v: int = scoreboard_get("#chain", "val");
134
- scoreboard_set("#chain", "val", v * 2);
152
+ let v: int = scoreboard_get("#chain", #val);
153
+ scoreboard_set("#chain", #val, v * 2);
135
154
  }
136
155
  fn step2() {
137
- let v: int = scoreboard_get("#chain", "val");
138
- scoreboard_set("#chain", "val", v + 5);
156
+ let v: int = scoreboard_get("#chain", #val);
157
+ scoreboard_set("#chain", #val, v + 5);
139
158
  step3();
140
159
  }
141
160
  fn step1() {
142
- scoreboard_set("#chain", "val", 10);
161
+ scoreboard_set("#chain", #val, 10);
143
162
  step2();
144
163
  }
145
164
  `, 'call_chain')
@@ -157,10 +176,10 @@ beforeAll(async () => {
157
176
  // Scenario E: for-range loop — loop counter increments exactly N times
158
177
  writeFixture(`
159
178
  fn count_to_five() {
160
- scoreboard_set("#range", "counter", 0);
179
+ scoreboard_set("#range", #counter, 0);
161
180
  for i in 0..5 {
162
- let c: int = scoreboard_get("#range", "counter");
163
- scoreboard_set("#range", "counter", c + 1);
181
+ let c: int = scoreboard_get("#range", #counter);
182
+ scoreboard_set("#range", #counter, c + 1);
164
183
  }
165
184
  }
166
185
  `, 'range_test')
@@ -172,7 +191,7 @@ beforeAll(async () => {
172
191
  }
173
192
  fn run_nested() {
174
193
  let a: int = triple(4);
175
- scoreboard_set("#nested", "result", a);
194
+ scoreboard_set("#nested", #result, a);
176
195
  }
177
196
  `, 'nested_test')
178
197
 
@@ -180,10 +199,10 @@ beforeAll(async () => {
180
199
  writeFixture(`
181
200
  fn classify(x: int) {
182
201
  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); }
202
+ 1 => { scoreboard_set("#match", #out, 10); }
203
+ 2 => { scoreboard_set("#match", #out, 20); }
204
+ 3 => { scoreboard_set("#match", #out, 30); }
205
+ _ => { scoreboard_set("#match", #out, -1); }
187
206
  }
188
207
  }
189
208
  `, 'match_test')
@@ -191,14 +210,14 @@ beforeAll(async () => {
191
210
  // Scenario H: while loop counts down
192
211
  writeFixture(`
193
212
  fn countdown() {
194
- scoreboard_set("#wloop", "i", 10);
195
- scoreboard_set("#wloop", "steps", 0);
196
- let i: int = scoreboard_get("#wloop", "i");
213
+ scoreboard_set("#wloop", #i, 10);
214
+ scoreboard_set("#wloop", #steps, 0);
215
+ let i: int = scoreboard_get("#wloop", #i);
197
216
  while (i > 0) {
198
- let s: int = scoreboard_get("#wloop", "steps");
199
- scoreboard_set("#wloop", "steps", s + 1);
217
+ let s: int = scoreboard_get("#wloop", #steps);
218
+ scoreboard_set("#wloop", #steps, s + 1);
200
219
  i = i - 1;
201
- scoreboard_set("#wloop", "i", i);
220
+ scoreboard_set("#wloop", #i, i);
202
221
  }
203
222
  }
204
223
  `, 'while_test')
@@ -206,17 +225,17 @@ beforeAll(async () => {
206
225
  // Scenario I: multiple if/else branches (boundary test)
207
226
  writeFixture(`
208
227
  fn classify_score() {
209
- let x: int = scoreboard_get("#boundary", "input");
228
+ let x: int = scoreboard_get("#boundary", #input);
210
229
  if (x > 100) {
211
- scoreboard_set("#boundary", "tier", 3);
230
+ scoreboard_set("#boundary", #tier, 3);
212
231
  } else {
213
232
  if (x > 50) {
214
- scoreboard_set("#boundary", "tier", 2);
233
+ scoreboard_set("#boundary", #tier, 2);
215
234
  } else {
216
235
  if (x > 0) {
217
- scoreboard_set("#boundary", "tier", 1);
236
+ scoreboard_set("#boundary", #tier, 1);
218
237
  } else {
219
- scoreboard_set("#boundary", "tier", 0);
238
+ scoreboard_set("#boundary", #tier, 0);
220
239
  }
221
240
  }
222
241
  }
@@ -238,34 +257,41 @@ beforeAll(async () => {
238
257
  let a: int = 2;
239
258
  let b: int = 3;
240
259
  let c: int = 4;
241
- scoreboard_set("#order", "r1", a + b * c);
242
- scoreboard_set("#order", "r2", (a + b) * c);
260
+ scoreboard_set("#order", #r1, a + b * c);
261
+ scoreboard_set("#order", #r2, (a + b) * c);
243
262
  let d: int = 100;
244
263
  let e: int = d / 3;
245
- scoreboard_set("#order", "r3", e);
264
+ scoreboard_set("#order", #r3, e);
246
265
  }
247
266
  `, 'order_test')
248
267
 
249
268
  // Scenario L: scoreboard read-modify-write chain
250
269
  writeFixture(`
251
270
  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);
271
+ scoreboard_set("#rmw", #v, 1);
272
+ let v: int = scoreboard_get("#rmw", #v);
273
+ scoreboard_set("#rmw", #v, v * 2);
274
+ v = scoreboard_get("#rmw", #v);
275
+ scoreboard_set("#rmw", #v, v * 2);
276
+ v = scoreboard_get("#rmw", #v);
277
+ scoreboard_set("#rmw", #v, v * 2);
259
278
  }
260
279
  `, 'rmw_test')
261
280
 
281
+ writeFixtureFile('impl-test.mcrs', 'impl_test')
282
+ writeFixtureFile('timeout-test.mcrs', 'timeout_test')
283
+ writeFixtureFile('interval-test.mcrs', 'interval_test')
284
+ writeFixtureFile('is-check-test.mcrs', 'is_check_test')
285
+ writeFixtureFile('event-test.mcrs', 'event_test')
286
+
262
287
  // ── Full reset + safe data reload ────────────────────────────────────
263
288
  await mc.fullReset()
264
289
 
265
290
  // Pre-create scoreboards
266
291
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
267
292
  'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
268
- 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v']) {
293
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v',
294
+ 'done', 'fired', 'players', 'zombies']) {
269
295
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => {})
270
296
  }
271
297
  await mc.command('/scoreboard players set counter ticks 0')
@@ -682,3 +708,92 @@ describe('E2E Scenario Tests', () => {
682
708
  })
683
709
 
684
710
  })
711
+
712
+ describe('MC Integration - New Features', () => {
713
+ test('impl-test.mcrs: Timer::new/start/tick/done works in-game', async () => {
714
+ if (!serverOnline) return
715
+
716
+ await mc.command('/scoreboard players set #impl done 0')
717
+ await mc.command('/scoreboard players set timer_ticks rs 0')
718
+ await mc.command('/scoreboard players set timer_active rs 0')
719
+
720
+ await mc.command('/function impl_test:__load').catch(() => {})
721
+ await mc.command('/function impl_test:test')
722
+ await mc.ticks(5)
723
+
724
+ const done = await mc.scoreboard('#impl', 'done')
725
+ const ticks = await mc.scoreboard('timer_ticks', 'rs')
726
+ expect(done).toBe(1)
727
+ expect(ticks).toBe(3)
728
+ })
729
+
730
+ test('timeout-test.mcrs: setTimeout executes after delay', async () => {
731
+ if (!serverOnline) return
732
+
733
+ await mc.command('/scoreboard players set #timeout fired 0')
734
+ await mc.command('/function timeout_test:__load').catch(() => {})
735
+ await mc.command('/function timeout_test:start')
736
+ await mc.ticks(10)
737
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(0)
738
+
739
+ await mc.ticks(15)
740
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(1)
741
+ })
742
+
743
+ test('interval-test.mcrs: setInterval repeats on schedule', async () => {
744
+ if (!serverOnline) return
745
+
746
+ await mc.command('/scoreboard players set #interval ticks 0')
747
+ await mc.command('/function interval_test:__load').catch(() => {})
748
+ await mc.command('/function interval_test:start')
749
+ await mc.ticks(70)
750
+
751
+ const count = await mc.scoreboard('#interval', 'ticks')
752
+ expect(count).toBeGreaterThanOrEqual(3)
753
+ expect(count).toBeLessThanOrEqual(3)
754
+ })
755
+
756
+ test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
757
+ if (!serverOnline) return
758
+
759
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
760
+ await mc.command('/scoreboard players set #is_check players 0')
761
+ await mc.command('/scoreboard players set #is_check zombies 0')
762
+ await mc.command('/function is_check_test:__load').catch(() => {})
763
+ await mc.command('/summon minecraft:zombie 0 65 0')
764
+ await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target')
765
+ await mc.command('/summon minecraft:armor_stand 2 65 0')
766
+ await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target')
767
+
768
+ await mc.command('/function is_check_test:check_types')
769
+ await mc.ticks(5)
770
+
771
+ const zombies = await mc.scoreboard('#is_check', 'zombies')
772
+ const players = await mc.scoreboard('#is_check', 'players')
773
+ const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]')
774
+ const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]')
775
+
776
+ expect(zombies).toBe(1)
777
+ expect(players).toBe(0)
778
+ expect(zombieEntities).toHaveLength(0)
779
+ expect(standEntities).toHaveLength(1)
780
+
781
+ await mc.command('/kill @e[tag=is_check_target]').catch(() => {})
782
+ })
783
+
784
+ test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
785
+ if (!serverOnline) return
786
+
787
+ // Verify the event system compiles correctly
788
+ await mc.command('/function event_test:__load').catch(() => {})
789
+ await mc.ticks(5)
790
+
791
+ // Verify the trigger function exists
792
+ const result = await mc.command('/function event_test:trigger_fake_death')
793
+ expect(result.ok).toBe(true)
794
+
795
+ // Verify __tick exists (event dispatcher)
796
+ const tickResult = await mc.command('/function event_test:__tick').catch(() => ({ ok: false }))
797
+ expect(tickResult.ok).toBe(true)
798
+ })
799
+ })
@@ -28,12 +28,12 @@ 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 $_0 rs run scoreboard players get config turret_range'
31
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.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)
35
35
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall))
36
- expect(loopBody).not.toContain('scoreboard players get config turret_range')
36
+ expect(loopBody).not.toContain('scoreboard players get config test.turret_range')
37
37
  })
38
38
  })
39
39
 
@@ -51,7 +51,7 @@ fn read_twice() {
51
51
 
52
52
  const result = compile(source, { namespace: 'test' })
53
53
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction')
54
- const readMatches = fn.match(/scoreboard players get @s coins/g) ?? []
54
+ const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? []
55
55
 
56
56
  expect(readMatches).toHaveLength(1)
57
57
  expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')