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.
- package/CHANGELOG.md +54 -0
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +164 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +8 -8
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +20 -1
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.js +25 -0
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.js +3 -2
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +439 -15
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +79 -10
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +178 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +8 -8
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +25 -1
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +27 -0
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/index.ts +4 -3
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +528 -16
- package/src/parser/index.ts +90 -12
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- 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
|
|
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",
|
|
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",
|
|
102
|
+
let x: int = scoreboard_get("#check_x", #test_score);
|
|
84
103
|
if (x > 5) {
|
|
85
|
-
scoreboard_set("#check_x",
|
|
104
|
+
scoreboard_set("#check_x", #result, 1);
|
|
86
105
|
} else {
|
|
87
|
-
scoreboard_set("#check_x",
|
|
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",
|
|
117
|
+
let time: int = scoreboard_get("#game", #timer);
|
|
99
118
|
if (time > 0) {
|
|
100
|
-
scoreboard_set("#game",
|
|
119
|
+
scoreboard_set("#game", #timer, time - 1);
|
|
101
120
|
}
|
|
102
121
|
if (time == 1) {
|
|
103
|
-
scoreboard_set("#game",
|
|
122
|
+
scoreboard_set("#game", #ended, 1);
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
fn start_game() {
|
|
107
|
-
scoreboard_set("#game",
|
|
108
|
-
scoreboard_set("#game",
|
|
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",
|
|
116
|
-
let b: int = scoreboard_get("#math",
|
|
117
|
-
scoreboard_set("#math",
|
|
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",
|
|
121
|
-
let y: int = scoreboard_get("#math",
|
|
122
|
-
scoreboard_set("#math",
|
|
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",
|
|
134
|
-
scoreboard_set("#chain",
|
|
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",
|
|
138
|
-
scoreboard_set("#chain",
|
|
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",
|
|
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",
|
|
179
|
+
scoreboard_set("#range", #counter, 0);
|
|
161
180
|
for i in 0..5 {
|
|
162
|
-
let c: int = scoreboard_get("#range",
|
|
163
|
-
scoreboard_set("#range",
|
|
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",
|
|
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",
|
|
184
|
-
2 => { scoreboard_set("#match",
|
|
185
|
-
3 => { scoreboard_set("#match",
|
|
186
|
-
_ => { scoreboard_set("#match",
|
|
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",
|
|
195
|
-
scoreboard_set("#wloop",
|
|
196
|
-
let i: int = scoreboard_get("#wloop",
|
|
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",
|
|
199
|
-
scoreboard_set("#wloop",
|
|
217
|
+
let s: int = scoreboard_get("#wloop", #steps);
|
|
218
|
+
scoreboard_set("#wloop", #steps, s + 1);
|
|
200
219
|
i = i - 1;
|
|
201
|
-
scoreboard_set("#wloop",
|
|
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",
|
|
228
|
+
let x: int = scoreboard_get("#boundary", #input);
|
|
210
229
|
if (x > 100) {
|
|
211
|
-
scoreboard_set("#boundary",
|
|
230
|
+
scoreboard_set("#boundary", #tier, 3);
|
|
212
231
|
} else {
|
|
213
232
|
if (x > 50) {
|
|
214
|
-
scoreboard_set("#boundary",
|
|
233
|
+
scoreboard_set("#boundary", #tier, 2);
|
|
215
234
|
} else {
|
|
216
235
|
if (x > 0) {
|
|
217
|
-
scoreboard_set("#boundary",
|
|
236
|
+
scoreboard_set("#boundary", #tier, 1);
|
|
218
237
|
} else {
|
|
219
|
-
scoreboard_set("#boundary",
|
|
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",
|
|
242
|
-
scoreboard_set("#order",
|
|
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",
|
|
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",
|
|
253
|
-
let v: int = scoreboard_get("#rmw",
|
|
254
|
-
scoreboard_set("#rmw",
|
|
255
|
-
v = scoreboard_get("#rmw",
|
|
256
|
-
scoreboard_set("#rmw",
|
|
257
|
-
v = scoreboard_get("#rmw",
|
|
258
|
-
scoreboard_set("#rmw",
|
|
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')
|