redscript-mc 1.1.0 → 1.2.1

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 (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -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', () => {
@@ -499,6 +582,15 @@ fn choose(dir: Direction) {
499
582
  expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]')
500
583
  })
501
584
 
585
+ it('lowers f-string output builtins to tellraw/title JSON components', () => {
586
+ const ir = compile('fn test() { let score: int = 7; say(f"Score: {score}"); tellraw(@a, f"Score: {score}"); actionbar(@s, f"Score: {score}"); title(@s, f"Score: {score}"); }')
587
+ const fn = getFunction(ir, 'test')!
588
+ const rawCmds = getRawCommands(fn)
589
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
590
+ expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
591
+ expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
592
+ })
593
+
502
594
  it('lowers summon()', () => {
503
595
  const ir = compile('fn test() { summon("zombie"); }')
504
596
  const fn = getFunction(ir, 'test')!
@@ -603,12 +695,12 @@ fn test() {
603
695
  `)
604
696
  const fn = getFunction(ir, 'test')!
605
697
  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')
698
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills')
699
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins')
700
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp')
609
701
  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')
702
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"')
703
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills')
612
704
  })
613
705
 
614
706
  it('lowers bossbar management builtins', () => {
@@ -682,6 +774,43 @@ fn test() {
682
774
  expect(rawCmds).toContain('random reset loot 42')
683
775
  })
684
776
 
777
+ it('lowers setTimeout() to a scheduled helper function', () => {
778
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }')
779
+ const fn = getFunction(ir, 'test')!
780
+ const timeoutFn = getFunction(ir, '__timeout_0')!
781
+ const rawCmds = getRawCommands(fn)
782
+ const timeoutCmds = getRawCommands(timeoutFn)
783
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
784
+ expect(timeoutCmds).toContain('say hi')
785
+ })
786
+
787
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
788
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }')
789
+ const fn = getFunction(ir, 'test')!
790
+ const intervalFn = getFunction(ir, '__interval_0')!
791
+ const intervalBodyFn = getFunction(ir, '__interval_body_0')!
792
+ const rawCmds = getRawCommands(fn)
793
+ const intervalCmds = getRawCommands(intervalFn)
794
+ const intervalBodyCmds = getRawCommands(intervalBodyFn)
795
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
796
+ expect(intervalCmds).toContain('function test:__interval_body_0')
797
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t')
798
+ expect(intervalBodyCmds).toContain('say tick')
799
+ })
800
+
801
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
802
+ const ir = compile(`
803
+ fn test() {
804
+ let intervalId: int = setInterval(20, () => { say("tick"); });
805
+ clearInterval(intervalId);
806
+ }
807
+ `)
808
+ const fn = getFunction(ir, 'test')!
809
+ const rawCmds = getRawCommands(fn)
810
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
811
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
812
+ })
813
+
685
814
  it('lowers data_get from entity', () => {
686
815
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }')
687
816
  const fn = getFunction(ir, 'test')!
@@ -736,7 +865,7 @@ fn test() {
736
865
  const fn = getFunction(ir, 'test')!
737
866
  const rawCmds = getRawCommands(fn)
738
867
  expect(rawCmds.some(cmd =>
739
- cmd.includes('run scoreboard players get @s score')
868
+ cmd.includes('run scoreboard players get @s test.score')
740
869
  )).toBe(true)
741
870
  })
742
871
 
@@ -744,7 +873,14 @@ fn test() {
744
873
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }')
745
874
  const fn = getFunction(ir, 'test')!
746
875
  const rawCmds = getRawCommands(fn)
747
- expect(rawCmds).toContain('scoreboard players set @a kills 0')
876
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0')
877
+ })
878
+
879
+ it('skips prefixing raw mc_name objectives', () => {
880
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }')
881
+ const fn = getFunction(ir, 'test')!
882
+ const rawCmds = getRawCommands(fn)
883
+ expect(rawCmds).toContain('scoreboard players set @s health 100')
748
884
  })
749
885
 
750
886
  it('warns on quoted selectors in scoreboard_get', () => {
@@ -752,7 +888,7 @@ fn test() {
752
888
  const fn = getFunction(ir, 'test')!
753
889
  const rawCmds = getRawCommands(fn)
754
890
  expect(rawCmds.some(cmd =>
755
- cmd.includes('run scoreboard players get @s score')
891
+ cmd.includes('run scoreboard players get @s test.score')
756
892
  )).toBe(true)
757
893
  expect(warnings).toContainEqual(expect.objectContaining({
758
894
  code: 'W_QUOTED_SELECTOR',
@@ -765,7 +901,7 @@ fn test() {
765
901
  const fn = getFunction(ir, 'test')!
766
902
  const rawCmds = getRawCommands(fn)
767
903
  expect(rawCmds.some(cmd =>
768
- cmd.includes('run scoreboard players get #global total')
904
+ cmd.includes('run scoreboard players get #global test.total')
769
905
  )).toBe(true)
770
906
  expect(warnings).toHaveLength(0)
771
907
  })
@@ -782,6 +918,40 @@ fn test() {
782
918
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
783
919
  }))
784
920
  })
921
+
922
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
923
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }')
924
+ const fn = getFunction(ir, 'test')!
925
+ const rawCmds = getRawCommands(fn)
926
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5')
927
+ })
928
+ })
929
+
930
+ describe('timer builtins', () => {
931
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
932
+ const ir = compile(`
933
+ fn test() {
934
+ let intervalId: int = setInterval(20, () => {
935
+ say("tick");
936
+ });
937
+ setTimeout(100, () => {
938
+ say("later");
939
+ });
940
+ clearInterval(intervalId);
941
+ }
942
+ `)
943
+ const fn = getFunction(ir, 'test')!
944
+ const rawCmds = getRawCommands(fn)
945
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
946
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
947
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
948
+
949
+ const intervalFn = getFunction(ir, '__interval_0')!
950
+ expect(getRawCommands(intervalFn)).toEqual([
951
+ 'function test:__interval_body_0',
952
+ 'schedule function test:__interval_0 20t',
953
+ ])
954
+ })
785
955
  })
786
956
 
787
957
  describe('decorators', () => {
@@ -803,6 +973,14 @@ fn test() {
803
973
  const fn = getFunction(ir, 'handle_advancement')!
804
974
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' })
805
975
  })
976
+
977
+ it('marks @on event functions and binds player to @s', () => {
978
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }')
979
+ const fn = getFunction(ir, 'handle_death')!
980
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' })
981
+ expect(fn.params).toEqual([])
982
+ expect(getRawCommands(fn)).toContain('tp @s @p')
983
+ })
806
984
  })
807
985
 
808
986
  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
+ })
@@ -62,6 +62,20 @@ fn chat() {
62
62
  expect(errors).toHaveLength(0)
63
63
  })
64
64
 
65
+ test('f-strings generate valid tellraw/title commands', () => {
66
+ const errors = validateSource(validator, `
67
+ fn chat() {
68
+ let score: int = 7;
69
+ say(f"You have {score} points");
70
+ tellraw(@a, f"Score: {score}");
71
+ actionbar(@s, f"Score: {score}");
72
+ title(@s, f"Score: {score}");
73
+ }
74
+ `, 'f-string')
75
+
76
+ expect(errors).toHaveLength(0)
77
+ })
78
+
65
79
  test('array operations generate valid data commands', () => {
66
80
  const errors = validateSource(validator, `
67
81
  fn arrays() {
@@ -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')