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