redscript-mc 1.2.10 → 1.2.11
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/dist/__tests__/mc-integration.test.js +85 -0
- package/dist/ast/types.d.ts +78 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lowering/index.js +59 -4
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +159 -18
- package/package.json +1 -1
- package/src/__tests__/fixtures/array-test.mcrs +30 -0
- package/src/__tests__/fixtures/break-continue-test.mcrs +46 -0
- package/src/__tests__/fixtures/enum-test.mcrs +37 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +33 -0
- package/src/__tests__/fixtures/match-range-test.mcrs +45 -0
- package/src/__tests__/fixtures/struct-test.mcrs +34 -0
- package/src/__tests__/mc-integration.test.ts +97 -0
- package/src/ast/types.ts +22 -1
- package/src/index.ts +1 -1
- package/src/lowering/index.ts +59 -4
- package/src/parser/index.ts +145 -18
|
@@ -731,4 +731,89 @@ describe('MC Integration - New Features', () => {
|
|
|
731
731
|
expect(tickResult.ok).toBe(true);
|
|
732
732
|
});
|
|
733
733
|
});
|
|
734
|
+
describe('MC Integration - Extended Coverage', () => {
|
|
735
|
+
test('struct-test.mcrs: struct instantiation and field access', async () => {
|
|
736
|
+
if (!serverOnline)
|
|
737
|
+
return;
|
|
738
|
+
writeFixtureFile('struct-test.mcrs', 'struct_test');
|
|
739
|
+
await mc.reload();
|
|
740
|
+
await mc.command('/function struct_test:__load').catch(() => { });
|
|
741
|
+
await mc.command('/function struct_test:test_struct');
|
|
742
|
+
await mc.ticks(5);
|
|
743
|
+
expect(await mc.scoreboard('#struct_x', 'rs')).toBe(10);
|
|
744
|
+
expect(await mc.scoreboard('#struct_y', 'rs')).toBe(64);
|
|
745
|
+
expect(await mc.scoreboard('#struct_z', 'rs')).toBe(-5);
|
|
746
|
+
expect(await mc.scoreboard('#struct_x2', 'rs')).toBe(15); // 10+5
|
|
747
|
+
expect(await mc.scoreboard('#struct_z2', 'rs')).toBe(-10); // -5*2
|
|
748
|
+
expect(await mc.scoreboard('#struct_alive', 'rs')).toBe(1);
|
|
749
|
+
expect(await mc.scoreboard('#struct_score', 'rs')).toBe(100);
|
|
750
|
+
});
|
|
751
|
+
test('enum-test.mcrs: enum values and match', async () => {
|
|
752
|
+
if (!serverOnline)
|
|
753
|
+
return;
|
|
754
|
+
writeFixtureFile('enum-test.mcrs', 'enum_test');
|
|
755
|
+
await mc.reload();
|
|
756
|
+
await mc.command('/function enum_test:__load').catch(() => { });
|
|
757
|
+
await mc.command('/function enum_test:test_enum');
|
|
758
|
+
await mc.ticks(5);
|
|
759
|
+
expect(await mc.scoreboard('#enum_phase', 'rs')).toBe(2); // Playing=2
|
|
760
|
+
expect(await mc.scoreboard('#enum_match', 'rs')).toBe(2); // matched Playing
|
|
761
|
+
expect(await mc.scoreboard('#enum_rank', 'rs')).toBe(10); // Diamond=10
|
|
762
|
+
expect(await mc.scoreboard('#enum_high', 'rs')).toBe(1); // Diamond > Gold
|
|
763
|
+
});
|
|
764
|
+
test('array-test.mcrs: array operations', async () => {
|
|
765
|
+
if (!serverOnline)
|
|
766
|
+
return;
|
|
767
|
+
writeFixtureFile('array-test.mcrs', 'array_test');
|
|
768
|
+
await mc.reload();
|
|
769
|
+
await mc.command('/function array_test:__load').catch(() => { });
|
|
770
|
+
await mc.command('/function array_test:test_array');
|
|
771
|
+
await mc.ticks(5);
|
|
772
|
+
expect(await mc.scoreboard('#arr_0', 'rs')).toBe(10);
|
|
773
|
+
expect(await mc.scoreboard('#arr_2', 'rs')).toBe(30);
|
|
774
|
+
expect(await mc.scoreboard('#arr_4', 'rs')).toBe(50);
|
|
775
|
+
expect(await mc.scoreboard('#arr_len', 'rs')).toBe(5);
|
|
776
|
+
expect(await mc.scoreboard('#arr_sum', 'rs')).toBe(150); // 10+20+30+40+50
|
|
777
|
+
expect(await mc.scoreboard('#arr_push', 'rs')).toBe(4); // [1,2,3,4].len
|
|
778
|
+
expect(await mc.scoreboard('#arr_pop', 'rs')).toBe(4); // popped value
|
|
779
|
+
});
|
|
780
|
+
test('break-continue-test.mcrs: break and continue statements', async () => {
|
|
781
|
+
if (!serverOnline)
|
|
782
|
+
return;
|
|
783
|
+
writeFixtureFile('break-continue-test.mcrs', 'break_continue_test');
|
|
784
|
+
await mc.reload();
|
|
785
|
+
await mc.command('/function break_continue_test:__load').catch(() => { });
|
|
786
|
+
await mc.command('/function break_continue_test:test_break_continue');
|
|
787
|
+
await mc.ticks(10);
|
|
788
|
+
expect(await mc.scoreboard('#break_at', 'rs')).toBe(5);
|
|
789
|
+
expect(await mc.scoreboard('#sum_evens', 'rs')).toBe(20); // 0+2+4+6+8
|
|
790
|
+
expect(await mc.scoreboard('#while_break', 'rs')).toBe(7);
|
|
791
|
+
expect(await mc.scoreboard('#nested_break', 'rs')).toBe(3); // outer completes 3 times
|
|
792
|
+
});
|
|
793
|
+
test('match-range-test.mcrs: match with range patterns', async () => {
|
|
794
|
+
if (!serverOnline)
|
|
795
|
+
return;
|
|
796
|
+
writeFixtureFile('match-range-test.mcrs', 'match_range_test');
|
|
797
|
+
await mc.reload();
|
|
798
|
+
await mc.command('/function match_range_test:__load').catch(() => { });
|
|
799
|
+
await mc.command('/function match_range_test:test_match_range');
|
|
800
|
+
await mc.ticks(5);
|
|
801
|
+
expect(await mc.scoreboard('#grade', 'rs')).toBe(4); // score=85 → B
|
|
802
|
+
expect(await mc.scoreboard('#boundary_59', 'rs')).toBe(1); // 59 matches 0..59
|
|
803
|
+
expect(await mc.scoreboard('#boundary_60', 'rs')).toBe(2); // 60 matches 60..100
|
|
804
|
+
expect(await mc.scoreboard('#neg_range', 'rs')).toBe(1); // -5 matches ..0
|
|
805
|
+
});
|
|
806
|
+
test('foreach-at-test.mcrs: foreach with at @s context', async () => {
|
|
807
|
+
if (!serverOnline)
|
|
808
|
+
return;
|
|
809
|
+
writeFixtureFile('foreach-at-test.mcrs', 'foreach_at_test');
|
|
810
|
+
await mc.reload();
|
|
811
|
+
await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
|
|
812
|
+
await mc.command('/function foreach_at_test:setup').catch(() => { });
|
|
813
|
+
await mc.command('/function foreach_at_test:test_foreach_at');
|
|
814
|
+
await mc.ticks(10);
|
|
815
|
+
expect(await mc.scoreboard('#foreach_count', 'rs')).toBe(3);
|
|
816
|
+
expect(await mc.scoreboard('#foreach_at_count', 'rs')).toBe(3);
|
|
817
|
+
});
|
|
818
|
+
});
|
|
734
819
|
//# sourceMappingURL=mc-integration.test.js.map
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -247,6 +247,45 @@ export type ExecuteSubcommand = {
|
|
|
247
247
|
} | {
|
|
248
248
|
kind: 'at';
|
|
249
249
|
selector: EntitySelector;
|
|
250
|
+
} | {
|
|
251
|
+
kind: 'positioned';
|
|
252
|
+
x: string;
|
|
253
|
+
y: string;
|
|
254
|
+
z: string;
|
|
255
|
+
} | {
|
|
256
|
+
kind: 'positioned_as';
|
|
257
|
+
selector: EntitySelector;
|
|
258
|
+
} | {
|
|
259
|
+
kind: 'rotated';
|
|
260
|
+
yaw: string;
|
|
261
|
+
pitch: string;
|
|
262
|
+
} | {
|
|
263
|
+
kind: 'rotated_as';
|
|
264
|
+
selector: EntitySelector;
|
|
265
|
+
} | {
|
|
266
|
+
kind: 'facing';
|
|
267
|
+
x: string;
|
|
268
|
+
y: string;
|
|
269
|
+
z: string;
|
|
270
|
+
} | {
|
|
271
|
+
kind: 'facing_entity';
|
|
272
|
+
selector: EntitySelector;
|
|
273
|
+
anchor: 'eyes' | 'feet';
|
|
274
|
+
} | {
|
|
275
|
+
kind: 'anchored';
|
|
276
|
+
anchor: 'eyes' | 'feet';
|
|
277
|
+
} | {
|
|
278
|
+
kind: 'align';
|
|
279
|
+
axes: string;
|
|
280
|
+
} | {
|
|
281
|
+
kind: 'in';
|
|
282
|
+
dimension: string;
|
|
283
|
+
} | {
|
|
284
|
+
kind: 'on';
|
|
285
|
+
relation: string;
|
|
286
|
+
} | {
|
|
287
|
+
kind: 'summon';
|
|
288
|
+
entity: string;
|
|
250
289
|
} | {
|
|
251
290
|
kind: 'if_entity';
|
|
252
291
|
selector?: EntitySelector;
|
|
@@ -258,8 +297,45 @@ export type ExecuteSubcommand = {
|
|
|
258
297
|
varName?: string;
|
|
259
298
|
filters?: SelectorFilter;
|
|
260
299
|
} | {
|
|
261
|
-
kind: '
|
|
262
|
-
|
|
300
|
+
kind: 'if_block';
|
|
301
|
+
pos: [string, string, string];
|
|
302
|
+
block: string;
|
|
303
|
+
} | {
|
|
304
|
+
kind: 'unless_block';
|
|
305
|
+
pos: [string, string, string];
|
|
306
|
+
block: string;
|
|
307
|
+
} | {
|
|
308
|
+
kind: 'if_score';
|
|
309
|
+
target: string;
|
|
310
|
+
targetObj: string;
|
|
311
|
+
op: string;
|
|
312
|
+
source: string;
|
|
313
|
+
sourceObj: string;
|
|
314
|
+
} | {
|
|
315
|
+
kind: 'unless_score';
|
|
316
|
+
target: string;
|
|
317
|
+
targetObj: string;
|
|
318
|
+
op: string;
|
|
319
|
+
source: string;
|
|
320
|
+
sourceObj: string;
|
|
321
|
+
} | {
|
|
322
|
+
kind: 'if_score_range';
|
|
323
|
+
target: string;
|
|
324
|
+
targetObj: string;
|
|
325
|
+
range: string;
|
|
326
|
+
} | {
|
|
327
|
+
kind: 'unless_score_range';
|
|
328
|
+
target: string;
|
|
329
|
+
targetObj: string;
|
|
330
|
+
range: string;
|
|
331
|
+
} | {
|
|
332
|
+
kind: 'store_result';
|
|
333
|
+
target: string;
|
|
334
|
+
targetObj: string;
|
|
335
|
+
} | {
|
|
336
|
+
kind: 'store_success';
|
|
337
|
+
target: string;
|
|
338
|
+
targetObj: string;
|
|
263
339
|
};
|
|
264
340
|
export type Stmt = {
|
|
265
341
|
kind: 'let';
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Main entry point for programmatic usage.
|
|
5
5
|
*/
|
|
6
|
-
export declare const version = "1.2.
|
|
6
|
+
export declare const version = "1.2.11";
|
|
7
7
|
import type { Warning } from './lowering';
|
|
8
8
|
import { DatapackFile } from './codegen/mcfunction';
|
|
9
9
|
import type { IRModule } from './ir/types';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.MCCommandValidator = exports.generateDatapack = exports.optimize = expor
|
|
|
9
9
|
exports.compile = compile;
|
|
10
10
|
exports.check = check;
|
|
11
11
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
|
-
exports.version = '1.2.
|
|
12
|
+
exports.version = '1.2.11';
|
|
13
13
|
const lexer_1 = require("./lexer");
|
|
14
14
|
const parser_1 = require("./parser");
|
|
15
15
|
const typechecker_1 = require("./typechecker");
|
package/dist/lowering/index.js
CHANGED
|
@@ -1006,18 +1006,52 @@ class Lowering {
|
|
|
1006
1006
|
const parts = ['execute'];
|
|
1007
1007
|
for (const sub of stmt.subcommands) {
|
|
1008
1008
|
switch (sub.kind) {
|
|
1009
|
+
// Context modifiers
|
|
1009
1010
|
case 'as':
|
|
1010
1011
|
parts.push(`as ${this.selectorToString(sub.selector)}`);
|
|
1011
1012
|
break;
|
|
1012
1013
|
case 'at':
|
|
1013
1014
|
parts.push(`at ${this.selectorToString(sub.selector)}`);
|
|
1014
1015
|
break;
|
|
1016
|
+
case 'positioned':
|
|
1017
|
+
parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`);
|
|
1018
|
+
break;
|
|
1019
|
+
case 'positioned_as':
|
|
1020
|
+
parts.push(`positioned as ${this.selectorToString(sub.selector)}`);
|
|
1021
|
+
break;
|
|
1022
|
+
case 'rotated':
|
|
1023
|
+
parts.push(`rotated ${sub.yaw} ${sub.pitch}`);
|
|
1024
|
+
break;
|
|
1025
|
+
case 'rotated_as':
|
|
1026
|
+
parts.push(`rotated as ${this.selectorToString(sub.selector)}`);
|
|
1027
|
+
break;
|
|
1028
|
+
case 'facing':
|
|
1029
|
+
parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`);
|
|
1030
|
+
break;
|
|
1031
|
+
case 'facing_entity':
|
|
1032
|
+
parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`);
|
|
1033
|
+
break;
|
|
1034
|
+
case 'anchored':
|
|
1035
|
+
parts.push(`anchored ${sub.anchor}`);
|
|
1036
|
+
break;
|
|
1037
|
+
case 'align':
|
|
1038
|
+
parts.push(`align ${sub.axes}`);
|
|
1039
|
+
break;
|
|
1040
|
+
case 'in':
|
|
1041
|
+
parts.push(`in ${sub.dimension}`);
|
|
1042
|
+
break;
|
|
1043
|
+
case 'on':
|
|
1044
|
+
parts.push(`on ${sub.relation}`);
|
|
1045
|
+
break;
|
|
1046
|
+
case 'summon':
|
|
1047
|
+
parts.push(`summon ${sub.entity}`);
|
|
1048
|
+
break;
|
|
1049
|
+
// Conditions
|
|
1015
1050
|
case 'if_entity':
|
|
1016
1051
|
if (sub.selector) {
|
|
1017
1052
|
parts.push(`if entity ${this.selectorToString(sub.selector)}`);
|
|
1018
1053
|
}
|
|
1019
1054
|
else if (sub.varName) {
|
|
1020
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1021
1055
|
const sel = { kind: '@s', filters: sub.filters };
|
|
1022
1056
|
parts.push(`if entity ${this.selectorToString(sel)}`);
|
|
1023
1057
|
}
|
|
@@ -1027,13 +1061,34 @@ class Lowering {
|
|
|
1027
1061
|
parts.push(`unless entity ${this.selectorToString(sub.selector)}`);
|
|
1028
1062
|
}
|
|
1029
1063
|
else if (sub.varName) {
|
|
1030
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1031
1064
|
const sel = { kind: '@s', filters: sub.filters };
|
|
1032
1065
|
parts.push(`unless entity ${this.selectorToString(sel)}`);
|
|
1033
1066
|
}
|
|
1034
1067
|
break;
|
|
1035
|
-
case '
|
|
1036
|
-
parts.push(`
|
|
1068
|
+
case 'if_block':
|
|
1069
|
+
parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
|
|
1070
|
+
break;
|
|
1071
|
+
case 'unless_block':
|
|
1072
|
+
parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
|
|
1073
|
+
break;
|
|
1074
|
+
case 'if_score':
|
|
1075
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
|
|
1076
|
+
break;
|
|
1077
|
+
case 'unless_score':
|
|
1078
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
|
|
1079
|
+
break;
|
|
1080
|
+
case 'if_score_range':
|
|
1081
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
|
|
1082
|
+
break;
|
|
1083
|
+
case 'unless_score_range':
|
|
1084
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
|
|
1085
|
+
break;
|
|
1086
|
+
// Store
|
|
1087
|
+
case 'store_result':
|
|
1088
|
+
parts.push(`store result score ${sub.target} ${sub.targetObj}`);
|
|
1089
|
+
break;
|
|
1090
|
+
case 'store_success':
|
|
1091
|
+
parts.push(`store success score ${sub.target} ${sub.targetObj}`);
|
|
1037
1092
|
break;
|
|
1038
1093
|
}
|
|
1039
1094
|
}
|
package/dist/parser/index.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export declare class Parser {
|
|
|
45
45
|
private parseAsStmt;
|
|
46
46
|
private parseAtStmt;
|
|
47
47
|
private parseExecuteStmt;
|
|
48
|
+
private parseExecuteCondition;
|
|
49
|
+
private parseCoordToken;
|
|
50
|
+
private parseBlockId;
|
|
51
|
+
private checkIdent;
|
|
48
52
|
private parseExprStmt;
|
|
49
53
|
private parseExpr;
|
|
50
54
|
private parseAssignment;
|
package/dist/parser/index.js
CHANGED
|
@@ -546,11 +546,11 @@ class Parser {
|
|
|
546
546
|
this.expect('in');
|
|
547
547
|
const iterable = this.parseExpr();
|
|
548
548
|
this.expect(')');
|
|
549
|
-
// Parse optional execute context modifiers (at, positioned, rotated, facing, etc.)
|
|
549
|
+
// Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
|
|
550
550
|
let executeContext;
|
|
551
|
-
// Check for
|
|
552
|
-
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align'];
|
|
553
|
-
if (this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
551
|
+
// Check for execute subcommand keywords
|
|
552
|
+
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon'];
|
|
553
|
+
if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
554
554
|
// Collect everything until we hit '{'
|
|
555
555
|
let context = '';
|
|
556
556
|
while (!this.check('{') && !this.check('eof')) {
|
|
@@ -615,34 +615,175 @@ class Parser {
|
|
|
615
615
|
const selector = this.parseSelector();
|
|
616
616
|
subcommands.push({ kind: 'at', selector });
|
|
617
617
|
}
|
|
618
|
-
else if (this.
|
|
619
|
-
|
|
620
|
-
if (this.
|
|
621
|
-
this.
|
|
618
|
+
else if (this.checkIdent('positioned')) {
|
|
619
|
+
this.advance();
|
|
620
|
+
if (this.match('as')) {
|
|
621
|
+
const selector = this.parseSelector();
|
|
622
|
+
subcommands.push({ kind: 'positioned_as', selector });
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const x = this.parseCoordToken();
|
|
626
|
+
const y = this.parseCoordToken();
|
|
627
|
+
const z = this.parseCoordToken();
|
|
628
|
+
subcommands.push({ kind: 'positioned', x, y, z });
|
|
622
629
|
}
|
|
623
|
-
const selectorOrVar = this.parseSelectorOrVarSelector();
|
|
624
|
-
subcommands.push({ kind: 'if_entity', ...selectorOrVar });
|
|
625
630
|
}
|
|
626
|
-
else if (this.
|
|
627
|
-
|
|
628
|
-
if (this.
|
|
629
|
-
this.
|
|
631
|
+
else if (this.checkIdent('rotated')) {
|
|
632
|
+
this.advance();
|
|
633
|
+
if (this.match('as')) {
|
|
634
|
+
const selector = this.parseSelector();
|
|
635
|
+
subcommands.push({ kind: 'rotated_as', selector });
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const yaw = this.parseCoordToken();
|
|
639
|
+
const pitch = this.parseCoordToken();
|
|
640
|
+
subcommands.push({ kind: 'rotated', yaw, pitch });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else if (this.checkIdent('facing')) {
|
|
644
|
+
this.advance();
|
|
645
|
+
if (this.checkIdent('entity')) {
|
|
646
|
+
this.advance();
|
|
647
|
+
const selector = this.parseSelector();
|
|
648
|
+
const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value : 'feet';
|
|
649
|
+
subcommands.push({ kind: 'facing_entity', selector, anchor });
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
const x = this.parseCoordToken();
|
|
653
|
+
const y = this.parseCoordToken();
|
|
654
|
+
const z = this.parseCoordToken();
|
|
655
|
+
subcommands.push({ kind: 'facing', x, y, z });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else if (this.checkIdent('anchored')) {
|
|
659
|
+
this.advance();
|
|
660
|
+
const anchor = this.advance().value;
|
|
661
|
+
subcommands.push({ kind: 'anchored', anchor });
|
|
662
|
+
}
|
|
663
|
+
else if (this.checkIdent('align')) {
|
|
664
|
+
this.advance();
|
|
665
|
+
const axes = this.advance().value;
|
|
666
|
+
subcommands.push({ kind: 'align', axes });
|
|
667
|
+
}
|
|
668
|
+
else if (this.checkIdent('on')) {
|
|
669
|
+
this.advance();
|
|
670
|
+
const relation = this.advance().value;
|
|
671
|
+
subcommands.push({ kind: 'on', relation });
|
|
672
|
+
}
|
|
673
|
+
else if (this.checkIdent('summon')) {
|
|
674
|
+
this.advance();
|
|
675
|
+
const entity = this.advance().value;
|
|
676
|
+
subcommands.push({ kind: 'summon', entity });
|
|
677
|
+
}
|
|
678
|
+
else if (this.checkIdent('store')) {
|
|
679
|
+
this.advance();
|
|
680
|
+
const storeType = this.advance().value; // 'result' or 'success'
|
|
681
|
+
if (this.checkIdent('score')) {
|
|
682
|
+
this.advance();
|
|
683
|
+
const target = this.advance().value;
|
|
684
|
+
const targetObj = this.advance().value;
|
|
685
|
+
if (storeType === 'result') {
|
|
686
|
+
subcommands.push({ kind: 'store_result', target, targetObj });
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
subcommands.push({ kind: 'store_success', target, targetObj });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
this.error('store currently only supports score target');
|
|
630
694
|
}
|
|
631
|
-
|
|
632
|
-
|
|
695
|
+
}
|
|
696
|
+
else if (this.match('if')) {
|
|
697
|
+
this.parseExecuteCondition(subcommands, 'if');
|
|
698
|
+
}
|
|
699
|
+
else if (this.match('unless')) {
|
|
700
|
+
this.parseExecuteCondition(subcommands, 'unless');
|
|
633
701
|
}
|
|
634
702
|
else if (this.match('in')) {
|
|
635
|
-
|
|
703
|
+
// Dimension can be namespaced: minecraft:the_nether
|
|
704
|
+
let dim = this.advance().value;
|
|
705
|
+
if (this.match(':')) {
|
|
706
|
+
dim += ':' + this.advance().value;
|
|
707
|
+
}
|
|
636
708
|
subcommands.push({ kind: 'in', dimension: dim });
|
|
637
709
|
}
|
|
638
710
|
else {
|
|
639
|
-
this.error(`Unexpected token in execute statement: ${this.peek().kind}`);
|
|
711
|
+
this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`);
|
|
640
712
|
}
|
|
641
713
|
}
|
|
642
714
|
this.expect('run');
|
|
643
715
|
const body = this.parseBlock();
|
|
644
716
|
return this.withLoc({ kind: 'execute', subcommands, body }, executeToken);
|
|
645
717
|
}
|
|
718
|
+
parseExecuteCondition(subcommands, type) {
|
|
719
|
+
if (this.checkIdent('entity') || this.check('selector')) {
|
|
720
|
+
if (this.checkIdent('entity'))
|
|
721
|
+
this.advance();
|
|
722
|
+
const selectorOrVar = this.parseSelectorOrVarSelector();
|
|
723
|
+
subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar });
|
|
724
|
+
}
|
|
725
|
+
else if (this.checkIdent('block')) {
|
|
726
|
+
this.advance();
|
|
727
|
+
const x = this.parseCoordToken();
|
|
728
|
+
const y = this.parseCoordToken();
|
|
729
|
+
const z = this.parseCoordToken();
|
|
730
|
+
const block = this.parseBlockId();
|
|
731
|
+
subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block });
|
|
732
|
+
}
|
|
733
|
+
else if (this.checkIdent('score')) {
|
|
734
|
+
this.advance();
|
|
735
|
+
const target = this.advance().value;
|
|
736
|
+
const targetObj = this.advance().value;
|
|
737
|
+
// Check for range or comparison
|
|
738
|
+
if (this.checkIdent('matches')) {
|
|
739
|
+
this.advance();
|
|
740
|
+
const range = this.advance().value;
|
|
741
|
+
subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range });
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
const op = this.advance().value; // <, <=, =, >=, >
|
|
745
|
+
const source = this.advance().value;
|
|
746
|
+
const sourceObj = this.advance().value;
|
|
747
|
+
subcommands.push({
|
|
748
|
+
kind: type === 'if' ? 'if_score' : 'unless_score',
|
|
749
|
+
target, targetObj, op, source, sourceObj
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
this.error(`Unknown condition type after ${type}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
parseCoordToken() {
|
|
758
|
+
// Handle ~, ^, numbers, relative coords like ~5, ^-3
|
|
759
|
+
const token = this.peek();
|
|
760
|
+
if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
|
|
761
|
+
token.kind === 'int_lit' || token.kind === 'float_lit' ||
|
|
762
|
+
token.kind === '-' || token.kind === 'ident') {
|
|
763
|
+
return this.advance().value;
|
|
764
|
+
}
|
|
765
|
+
this.error(`Expected coordinate, got ${token.kind}`);
|
|
766
|
+
return '~';
|
|
767
|
+
}
|
|
768
|
+
parseBlockId() {
|
|
769
|
+
// Parse block ID like minecraft:stone or stone
|
|
770
|
+
let id = this.advance().value;
|
|
771
|
+
if (this.match(':')) {
|
|
772
|
+
id += ':' + this.advance().value;
|
|
773
|
+
}
|
|
774
|
+
// Handle block states [facing=north]
|
|
775
|
+
if (this.check('[')) {
|
|
776
|
+
id += this.advance().value; // [
|
|
777
|
+
while (!this.check(']') && !this.check('eof')) {
|
|
778
|
+
id += this.advance().value;
|
|
779
|
+
}
|
|
780
|
+
id += this.advance().value; // ]
|
|
781
|
+
}
|
|
782
|
+
return id;
|
|
783
|
+
}
|
|
784
|
+
checkIdent(value) {
|
|
785
|
+
return this.check('ident') && this.peek().value === value;
|
|
786
|
+
}
|
|
646
787
|
parseExprStmt() {
|
|
647
788
|
const expr = this.parseExpr();
|
|
648
789
|
this.expect(';');
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Array operations test
|
|
2
|
+
|
|
3
|
+
@keep fn test_array() {
|
|
4
|
+
// Array initialization
|
|
5
|
+
let nums: int[] = [10, 20, 30, 40, 50];
|
|
6
|
+
|
|
7
|
+
// Array access
|
|
8
|
+
scoreboard_set("#arr_0", #rs, nums[0]);
|
|
9
|
+
scoreboard_set("#arr_2", #rs, nums[2]);
|
|
10
|
+
scoreboard_set("#arr_4", #rs, nums[4]);
|
|
11
|
+
|
|
12
|
+
// Array length via .len property
|
|
13
|
+
scoreboard_set("#arr_len", #rs, nums.len);
|
|
14
|
+
|
|
15
|
+
// Sum via foreach
|
|
16
|
+
let sum: int = 0;
|
|
17
|
+
foreach (n in nums) {
|
|
18
|
+
sum = sum + n;
|
|
19
|
+
}
|
|
20
|
+
scoreboard_set("#arr_sum", #rs, sum);
|
|
21
|
+
|
|
22
|
+
// Push operation
|
|
23
|
+
let arr2: int[] = [1, 2, 3];
|
|
24
|
+
arr2.push(4);
|
|
25
|
+
scoreboard_set("#arr_push", #rs, arr2.len);
|
|
26
|
+
|
|
27
|
+
// Pop operation
|
|
28
|
+
let popped: int = arr2.pop();
|
|
29
|
+
scoreboard_set("#arr_pop", #rs, popped);
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Break and continue statements test
|
|
2
|
+
|
|
3
|
+
@keep fn test_break_continue() {
|
|
4
|
+
// Test break - should stop at i=5
|
|
5
|
+
let break_at: int = -1;
|
|
6
|
+
for (let i: int = 0; i < 10; i = i + 1) {
|
|
7
|
+
if (i == 5) {
|
|
8
|
+
break_at = i;
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
scoreboard_set("#break_at", #rs, break_at);
|
|
13
|
+
|
|
14
|
+
// Test continue - sum only even numbers
|
|
15
|
+
let sum_evens: int = 0;
|
|
16
|
+
for (let i: int = 0; i < 10; i = i + 1) {
|
|
17
|
+
if (i % 2 != 0) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
sum_evens = sum_evens + i;
|
|
21
|
+
}
|
|
22
|
+
// 0+2+4+6+8 = 20
|
|
23
|
+
scoreboard_set("#sum_evens", #rs, sum_evens);
|
|
24
|
+
|
|
25
|
+
// While with break
|
|
26
|
+
let count: int = 0;
|
|
27
|
+
while (true) {
|
|
28
|
+
count = count + 1;
|
|
29
|
+
if (count >= 7) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
scoreboard_set("#while_break", #rs, count);
|
|
34
|
+
|
|
35
|
+
// Nested loop break (breaks inner only)
|
|
36
|
+
let outer_count: int = 0;
|
|
37
|
+
for (let a: int = 0; a < 3; a = a + 1) {
|
|
38
|
+
for (let b: int = 0; b < 10; b = b + 1) {
|
|
39
|
+
if (b == 2) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
outer_count = outer_count + 1;
|
|
44
|
+
}
|
|
45
|
+
scoreboard_set("#nested_break", #rs, outer_count);
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Enum definition and matching test
|
|
2
|
+
|
|
3
|
+
enum GamePhase {
|
|
4
|
+
Lobby, // 0
|
|
5
|
+
Starting, // 1
|
|
6
|
+
Playing, // 2
|
|
7
|
+
Ended // 3
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum Rank {
|
|
11
|
+
Bronze = 1,
|
|
12
|
+
Silver = 2,
|
|
13
|
+
Gold = 3,
|
|
14
|
+
Diamond = 10
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keep fn test_enum() {
|
|
18
|
+
// Basic enum value (use . not ::)
|
|
19
|
+
let phase: int = GamePhase.Playing;
|
|
20
|
+
scoreboard_set("#enum_phase", #rs, phase);
|
|
21
|
+
|
|
22
|
+
// Match on enum
|
|
23
|
+
match (phase) {
|
|
24
|
+
GamePhase.Lobby => { scoreboard_set("#enum_match", #rs, 0); }
|
|
25
|
+
GamePhase.Playing => { scoreboard_set("#enum_match", #rs, 2); }
|
|
26
|
+
_ => { scoreboard_set("#enum_match", #rs, -1); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Custom values
|
|
30
|
+
let rank: int = Rank.Diamond;
|
|
31
|
+
scoreboard_set("#enum_rank", #rs, rank);
|
|
32
|
+
|
|
33
|
+
// Comparison
|
|
34
|
+
if (rank > Rank.Gold) {
|
|
35
|
+
scoreboard_set("#enum_high", #rs, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Foreach with execute context modifiers test
|
|
2
|
+
|
|
3
|
+
@load fn setup() {
|
|
4
|
+
scoreboard_add("rs");
|
|
5
|
+
scoreboard_set("#foreach_count", #rs, 0);
|
|
6
|
+
scoreboard_set("#foreach_at_count", #rs, 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@keep fn test_foreach_at() {
|
|
10
|
+
// Spawn test entities
|
|
11
|
+
raw("summon minecraft:armor_stand ~ ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
12
|
+
raw("summon minecraft:armor_stand ~2 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
13
|
+
raw("summon minecraft:armor_stand ~4 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
14
|
+
|
|
15
|
+
// Basic foreach
|
|
16
|
+
let count: int = 0;
|
|
17
|
+
foreach (e in @e[type=armor_stand,tag=test_foreach]) {
|
|
18
|
+
count = count + 1;
|
|
19
|
+
}
|
|
20
|
+
scoreboard_set("#foreach_count", #rs, count);
|
|
21
|
+
|
|
22
|
+
// Foreach with at @s (execute at entity position)
|
|
23
|
+
let at_count: int = 0;
|
|
24
|
+
foreach (e in @e[type=armor_stand,tag=test_foreach]) at @s {
|
|
25
|
+
// This runs at each entity's position
|
|
26
|
+
at_count = at_count + 1;
|
|
27
|
+
raw("particle minecraft:heart ~ ~1 ~ 0 0 0 0 1");
|
|
28
|
+
}
|
|
29
|
+
scoreboard_set("#foreach_at_count", #rs, at_count);
|
|
30
|
+
|
|
31
|
+
// Cleanup
|
|
32
|
+
raw("kill @e[type=armor_stand,tag=test_foreach]");
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Match with range patterns test
|
|
2
|
+
|
|
3
|
+
@keep fn test_match_range() {
|
|
4
|
+
// Test score grading
|
|
5
|
+
let score: int = 85;
|
|
6
|
+
let grade: int = 0;
|
|
7
|
+
|
|
8
|
+
match (score) {
|
|
9
|
+
0..59 => { grade = 1; } // F
|
|
10
|
+
60..69 => { grade = 2; } // D
|
|
11
|
+
70..79 => { grade = 3; } // C
|
|
12
|
+
80..89 => { grade = 4; } // B
|
|
13
|
+
90..100 => { grade = 5; } // A
|
|
14
|
+
_ => { grade = 0; }
|
|
15
|
+
}
|
|
16
|
+
scoreboard_set("#grade", #rs, grade);
|
|
17
|
+
|
|
18
|
+
// Test boundary values
|
|
19
|
+
let val1: int = 59;
|
|
20
|
+
let result1: int = 0;
|
|
21
|
+
match (val1) {
|
|
22
|
+
0..59 => { result1 = 1; }
|
|
23
|
+
60..100 => { result1 = 2; }
|
|
24
|
+
_ => { result1 = 0; }
|
|
25
|
+
}
|
|
26
|
+
scoreboard_set("#boundary_59", #rs, result1);
|
|
27
|
+
|
|
28
|
+
let val2: int = 60;
|
|
29
|
+
let result2: int = 0;
|
|
30
|
+
match (val2) {
|
|
31
|
+
0..59 => { result2 = 1; }
|
|
32
|
+
60..100 => { result2 = 2; }
|
|
33
|
+
_ => { result2 = 0; }
|
|
34
|
+
}
|
|
35
|
+
scoreboard_set("#boundary_60", #rs, result2);
|
|
36
|
+
|
|
37
|
+
// Open-ended ranges
|
|
38
|
+
let neg: int = -5;
|
|
39
|
+
let neg_result: int = 0;
|
|
40
|
+
match (neg) {
|
|
41
|
+
..0 => { neg_result = 1; }
|
|
42
|
+
0.. => { neg_result = 2; }
|
|
43
|
+
}
|
|
44
|
+
scoreboard_set("#neg_range", #rs, neg_result);
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Struct instantiation and field access test
|
|
2
|
+
|
|
3
|
+
struct Point {
|
|
4
|
+
x: int,
|
|
5
|
+
y: int,
|
|
6
|
+
z: int
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
struct Player {
|
|
10
|
+
score: int,
|
|
11
|
+
alive: bool
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@keep fn test_struct() {
|
|
15
|
+
// Create struct instance
|
|
16
|
+
let p: Point = { x: 10, y: 64, z: -5 };
|
|
17
|
+
|
|
18
|
+
// Access fields
|
|
19
|
+
scoreboard_set("#struct_x", #rs, p.x);
|
|
20
|
+
scoreboard_set("#struct_y", #rs, p.y);
|
|
21
|
+
scoreboard_set("#struct_z", #rs, p.z);
|
|
22
|
+
|
|
23
|
+
// Modify via new instance
|
|
24
|
+
let p2: Point = { x: p.x + 5, y: p.y, z: p.z * 2 };
|
|
25
|
+
scoreboard_set("#struct_x2", #rs, p2.x);
|
|
26
|
+
scoreboard_set("#struct_z2", #rs, p2.z);
|
|
27
|
+
|
|
28
|
+
// Bool field
|
|
29
|
+
let player: Player = { score: 100, alive: true };
|
|
30
|
+
if (player.alive) {
|
|
31
|
+
scoreboard_set("#struct_alive", #rs, 1);
|
|
32
|
+
}
|
|
33
|
+
scoreboard_set("#struct_score", #rs, player.score);
|
|
34
|
+
}
|
|
@@ -797,3 +797,100 @@ describe('MC Integration - New Features', () => {
|
|
|
797
797
|
expect(tickResult.ok).toBe(true)
|
|
798
798
|
})
|
|
799
799
|
})
|
|
800
|
+
|
|
801
|
+
describe('MC Integration - Extended Coverage', () => {
|
|
802
|
+
test('struct-test.mcrs: struct instantiation and field access', async () => {
|
|
803
|
+
if (!serverOnline) return
|
|
804
|
+
|
|
805
|
+
writeFixtureFile('struct-test.mcrs', 'struct_test')
|
|
806
|
+
await mc.reload()
|
|
807
|
+
await mc.command('/function struct_test:__load').catch(() => {})
|
|
808
|
+
await mc.command('/function struct_test:test_struct')
|
|
809
|
+
await mc.ticks(5)
|
|
810
|
+
|
|
811
|
+
expect(await mc.scoreboard('#struct_x', 'rs')).toBe(10)
|
|
812
|
+
expect(await mc.scoreboard('#struct_y', 'rs')).toBe(64)
|
|
813
|
+
expect(await mc.scoreboard('#struct_z', 'rs')).toBe(-5)
|
|
814
|
+
expect(await mc.scoreboard('#struct_x2', 'rs')).toBe(15) // 10+5
|
|
815
|
+
expect(await mc.scoreboard('#struct_z2', 'rs')).toBe(-10) // -5*2
|
|
816
|
+
expect(await mc.scoreboard('#struct_alive', 'rs')).toBe(1)
|
|
817
|
+
expect(await mc.scoreboard('#struct_score', 'rs')).toBe(100)
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
test('enum-test.mcrs: enum values and match', async () => {
|
|
821
|
+
if (!serverOnline) return
|
|
822
|
+
|
|
823
|
+
writeFixtureFile('enum-test.mcrs', 'enum_test')
|
|
824
|
+
await mc.reload()
|
|
825
|
+
await mc.command('/function enum_test:__load').catch(() => {})
|
|
826
|
+
await mc.command('/function enum_test:test_enum')
|
|
827
|
+
await mc.ticks(5)
|
|
828
|
+
|
|
829
|
+
expect(await mc.scoreboard('#enum_phase', 'rs')).toBe(2) // Playing=2
|
|
830
|
+
expect(await mc.scoreboard('#enum_match', 'rs')).toBe(2) // matched Playing
|
|
831
|
+
expect(await mc.scoreboard('#enum_rank', 'rs')).toBe(10) // Diamond=10
|
|
832
|
+
expect(await mc.scoreboard('#enum_high', 'rs')).toBe(1) // Diamond > Gold
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
test('array-test.mcrs: array operations', async () => {
|
|
836
|
+
if (!serverOnline) return
|
|
837
|
+
|
|
838
|
+
writeFixtureFile('array-test.mcrs', 'array_test')
|
|
839
|
+
await mc.reload()
|
|
840
|
+
await mc.command('/function array_test:__load').catch(() => {})
|
|
841
|
+
await mc.command('/function array_test:test_array')
|
|
842
|
+
await mc.ticks(5)
|
|
843
|
+
|
|
844
|
+
expect(await mc.scoreboard('#arr_0', 'rs')).toBe(10)
|
|
845
|
+
expect(await mc.scoreboard('#arr_2', 'rs')).toBe(30)
|
|
846
|
+
expect(await mc.scoreboard('#arr_4', 'rs')).toBe(50)
|
|
847
|
+
expect(await mc.scoreboard('#arr_len', 'rs')).toBe(5)
|
|
848
|
+
expect(await mc.scoreboard('#arr_sum', 'rs')).toBe(150) // 10+20+30+40+50
|
|
849
|
+
expect(await mc.scoreboard('#arr_push', 'rs')).toBe(4) // [1,2,3,4].len
|
|
850
|
+
expect(await mc.scoreboard('#arr_pop', 'rs')).toBe(4) // popped value
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
test('break-continue-test.mcrs: break and continue statements', async () => {
|
|
854
|
+
if (!serverOnline) return
|
|
855
|
+
|
|
856
|
+
writeFixtureFile('break-continue-test.mcrs', 'break_continue_test')
|
|
857
|
+
await mc.reload()
|
|
858
|
+
await mc.command('/function break_continue_test:__load').catch(() => {})
|
|
859
|
+
await mc.command('/function break_continue_test:test_break_continue')
|
|
860
|
+
await mc.ticks(10)
|
|
861
|
+
|
|
862
|
+
expect(await mc.scoreboard('#break_at', 'rs')).toBe(5)
|
|
863
|
+
expect(await mc.scoreboard('#sum_evens', 'rs')).toBe(20) // 0+2+4+6+8
|
|
864
|
+
expect(await mc.scoreboard('#while_break', 'rs')).toBe(7)
|
|
865
|
+
expect(await mc.scoreboard('#nested_break', 'rs')).toBe(3) // outer completes 3 times
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
test('match-range-test.mcrs: match with range patterns', async () => {
|
|
869
|
+
if (!serverOnline) return
|
|
870
|
+
|
|
871
|
+
writeFixtureFile('match-range-test.mcrs', 'match_range_test')
|
|
872
|
+
await mc.reload()
|
|
873
|
+
await mc.command('/function match_range_test:__load').catch(() => {})
|
|
874
|
+
await mc.command('/function match_range_test:test_match_range')
|
|
875
|
+
await mc.ticks(5)
|
|
876
|
+
|
|
877
|
+
expect(await mc.scoreboard('#grade', 'rs')).toBe(4) // score=85 → B
|
|
878
|
+
expect(await mc.scoreboard('#boundary_59', 'rs')).toBe(1) // 59 matches 0..59
|
|
879
|
+
expect(await mc.scoreboard('#boundary_60', 'rs')).toBe(2) // 60 matches 60..100
|
|
880
|
+
expect(await mc.scoreboard('#neg_range', 'rs')).toBe(1) // -5 matches ..0
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
test('foreach-at-test.mcrs: foreach with at @s context', async () => {
|
|
884
|
+
if (!serverOnline) return
|
|
885
|
+
|
|
886
|
+
writeFixtureFile('foreach-at-test.mcrs', 'foreach_at_test')
|
|
887
|
+
await mc.reload()
|
|
888
|
+
await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
|
|
889
|
+
await mc.command('/function foreach_at_test:setup').catch(() => {})
|
|
890
|
+
await mc.command('/function foreach_at_test:test_foreach_at')
|
|
891
|
+
await mc.ticks(10)
|
|
892
|
+
|
|
893
|
+
expect(await mc.scoreboard('#foreach_count', 'rs')).toBe(3)
|
|
894
|
+
expect(await mc.scoreboard('#foreach_at_count', 'rs')).toBe(3)
|
|
895
|
+
})
|
|
896
|
+
})
|
package/src/ast/types.ts
CHANGED
|
@@ -179,11 +179,32 @@ export type LiteralExpr =
|
|
|
179
179
|
// ---------------------------------------------------------------------------
|
|
180
180
|
|
|
181
181
|
export type ExecuteSubcommand =
|
|
182
|
+
// Context modifiers
|
|
182
183
|
| { kind: 'as'; selector: EntitySelector }
|
|
183
184
|
| { kind: 'at'; selector: EntitySelector }
|
|
185
|
+
| { kind: 'positioned'; x: string; y: string; z: string }
|
|
186
|
+
| { kind: 'positioned_as'; selector: EntitySelector }
|
|
187
|
+
| { kind: 'rotated'; yaw: string; pitch: string }
|
|
188
|
+
| { kind: 'rotated_as'; selector: EntitySelector }
|
|
189
|
+
| { kind: 'facing'; x: string; y: string; z: string }
|
|
190
|
+
| { kind: 'facing_entity'; selector: EntitySelector; anchor: 'eyes' | 'feet' }
|
|
191
|
+
| { kind: 'anchored'; anchor: 'eyes' | 'feet' }
|
|
192
|
+
| { kind: 'align'; axes: string }
|
|
193
|
+
| { kind: 'in'; dimension: string }
|
|
194
|
+
| { kind: 'on'; relation: string }
|
|
195
|
+
| { kind: 'summon'; entity: string }
|
|
196
|
+
// Conditions
|
|
184
197
|
| { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
185
198
|
| { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
186
|
-
| { kind: '
|
|
199
|
+
| { kind: 'if_block'; pos: [string, string, string]; block: string }
|
|
200
|
+
| { kind: 'unless_block'; pos: [string, string, string]; block: string }
|
|
201
|
+
| { kind: 'if_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
|
|
202
|
+
| { kind: 'unless_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
|
|
203
|
+
| { kind: 'if_score_range'; target: string; targetObj: string; range: string }
|
|
204
|
+
| { kind: 'unless_score_range'; target: string; targetObj: string; range: string }
|
|
205
|
+
// Store
|
|
206
|
+
| { kind: 'store_result'; target: string; targetObj: string }
|
|
207
|
+
| { kind: 'store_success'; target: string; targetObj: string }
|
|
187
208
|
|
|
188
209
|
export type Stmt =
|
|
189
210
|
| { kind: 'let'; name: string; type?: TypeNode; init: Expr; span?: Span }
|
package/src/index.ts
CHANGED
package/src/lowering/index.ts
CHANGED
|
@@ -1200,17 +1200,51 @@ export class Lowering {
|
|
|
1200
1200
|
const parts: string[] = ['execute']
|
|
1201
1201
|
for (const sub of stmt.subcommands) {
|
|
1202
1202
|
switch (sub.kind) {
|
|
1203
|
+
// Context modifiers
|
|
1203
1204
|
case 'as':
|
|
1204
1205
|
parts.push(`as ${this.selectorToString(sub.selector)}`)
|
|
1205
1206
|
break
|
|
1206
1207
|
case 'at':
|
|
1207
1208
|
parts.push(`at ${this.selectorToString(sub.selector)}`)
|
|
1208
1209
|
break
|
|
1210
|
+
case 'positioned':
|
|
1211
|
+
parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`)
|
|
1212
|
+
break
|
|
1213
|
+
case 'positioned_as':
|
|
1214
|
+
parts.push(`positioned as ${this.selectorToString(sub.selector)}`)
|
|
1215
|
+
break
|
|
1216
|
+
case 'rotated':
|
|
1217
|
+
parts.push(`rotated ${sub.yaw} ${sub.pitch}`)
|
|
1218
|
+
break
|
|
1219
|
+
case 'rotated_as':
|
|
1220
|
+
parts.push(`rotated as ${this.selectorToString(sub.selector)}`)
|
|
1221
|
+
break
|
|
1222
|
+
case 'facing':
|
|
1223
|
+
parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`)
|
|
1224
|
+
break
|
|
1225
|
+
case 'facing_entity':
|
|
1226
|
+
parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`)
|
|
1227
|
+
break
|
|
1228
|
+
case 'anchored':
|
|
1229
|
+
parts.push(`anchored ${sub.anchor}`)
|
|
1230
|
+
break
|
|
1231
|
+
case 'align':
|
|
1232
|
+
parts.push(`align ${sub.axes}`)
|
|
1233
|
+
break
|
|
1234
|
+
case 'in':
|
|
1235
|
+
parts.push(`in ${sub.dimension}`)
|
|
1236
|
+
break
|
|
1237
|
+
case 'on':
|
|
1238
|
+
parts.push(`on ${sub.relation}`)
|
|
1239
|
+
break
|
|
1240
|
+
case 'summon':
|
|
1241
|
+
parts.push(`summon ${sub.entity}`)
|
|
1242
|
+
break
|
|
1243
|
+
// Conditions
|
|
1209
1244
|
case 'if_entity':
|
|
1210
1245
|
if (sub.selector) {
|
|
1211
1246
|
parts.push(`if entity ${this.selectorToString(sub.selector)}`)
|
|
1212
1247
|
} else if (sub.varName) {
|
|
1213
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1214
1248
|
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
1215
1249
|
parts.push(`if entity ${this.selectorToString(sel)}`)
|
|
1216
1250
|
}
|
|
@@ -1219,13 +1253,34 @@ export class Lowering {
|
|
|
1219
1253
|
if (sub.selector) {
|
|
1220
1254
|
parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
|
|
1221
1255
|
} else if (sub.varName) {
|
|
1222
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1223
1256
|
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
1224
1257
|
parts.push(`unless entity ${this.selectorToString(sel)}`)
|
|
1225
1258
|
}
|
|
1226
1259
|
break
|
|
1227
|
-
case '
|
|
1228
|
-
parts.push(`
|
|
1260
|
+
case 'if_block':
|
|
1261
|
+
parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
|
|
1262
|
+
break
|
|
1263
|
+
case 'unless_block':
|
|
1264
|
+
parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
|
|
1265
|
+
break
|
|
1266
|
+
case 'if_score':
|
|
1267
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
|
|
1268
|
+
break
|
|
1269
|
+
case 'unless_score':
|
|
1270
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
|
|
1271
|
+
break
|
|
1272
|
+
case 'if_score_range':
|
|
1273
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
|
|
1274
|
+
break
|
|
1275
|
+
case 'unless_score_range':
|
|
1276
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
|
|
1277
|
+
break
|
|
1278
|
+
// Store
|
|
1279
|
+
case 'store_result':
|
|
1280
|
+
parts.push(`store result score ${sub.target} ${sub.targetObj}`)
|
|
1281
|
+
break
|
|
1282
|
+
case 'store_success':
|
|
1283
|
+
parts.push(`store success score ${sub.target} ${sub.targetObj}`)
|
|
1229
1284
|
break
|
|
1230
1285
|
}
|
|
1231
1286
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -660,11 +660,11 @@ export class Parser {
|
|
|
660
660
|
const iterable = this.parseExpr()
|
|
661
661
|
this.expect(')')
|
|
662
662
|
|
|
663
|
-
// Parse optional execute context modifiers (at, positioned, rotated, facing, etc.)
|
|
663
|
+
// Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
|
|
664
664
|
let executeContext: string | undefined
|
|
665
|
-
// Check for
|
|
666
|
-
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align']
|
|
667
|
-
if (this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
665
|
+
// Check for execute subcommand keywords
|
|
666
|
+
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon']
|
|
667
|
+
if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
668
668
|
// Collect everything until we hit '{'
|
|
669
669
|
let context = ''
|
|
670
670
|
while (!this.check('{') && !this.check('eof')) {
|
|
@@ -738,25 +738,84 @@ export class Parser {
|
|
|
738
738
|
} else if (this.match('at')) {
|
|
739
739
|
const selector = this.parseSelector()
|
|
740
740
|
subcommands.push({ kind: 'at', selector })
|
|
741
|
-
} else if (this.
|
|
742
|
-
|
|
743
|
-
if (this.
|
|
744
|
-
this.
|
|
741
|
+
} else if (this.checkIdent('positioned')) {
|
|
742
|
+
this.advance()
|
|
743
|
+
if (this.match('as')) {
|
|
744
|
+
const selector = this.parseSelector()
|
|
745
|
+
subcommands.push({ kind: 'positioned_as', selector })
|
|
746
|
+
} else {
|
|
747
|
+
const x = this.parseCoordToken()
|
|
748
|
+
const y = this.parseCoordToken()
|
|
749
|
+
const z = this.parseCoordToken()
|
|
750
|
+
subcommands.push({ kind: 'positioned', x, y, z })
|
|
745
751
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
+
} else if (this.checkIdent('rotated')) {
|
|
753
|
+
this.advance()
|
|
754
|
+
if (this.match('as')) {
|
|
755
|
+
const selector = this.parseSelector()
|
|
756
|
+
subcommands.push({ kind: 'rotated_as', selector })
|
|
757
|
+
} else {
|
|
758
|
+
const yaw = this.parseCoordToken()
|
|
759
|
+
const pitch = this.parseCoordToken()
|
|
760
|
+
subcommands.push({ kind: 'rotated', yaw, pitch })
|
|
752
761
|
}
|
|
753
|
-
|
|
754
|
-
|
|
762
|
+
} else if (this.checkIdent('facing')) {
|
|
763
|
+
this.advance()
|
|
764
|
+
if (this.checkIdent('entity')) {
|
|
765
|
+
this.advance()
|
|
766
|
+
const selector = this.parseSelector()
|
|
767
|
+
const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value as 'eyes' | 'feet' : 'feet'
|
|
768
|
+
subcommands.push({ kind: 'facing_entity', selector, anchor })
|
|
769
|
+
} else {
|
|
770
|
+
const x = this.parseCoordToken()
|
|
771
|
+
const y = this.parseCoordToken()
|
|
772
|
+
const z = this.parseCoordToken()
|
|
773
|
+
subcommands.push({ kind: 'facing', x, y, z })
|
|
774
|
+
}
|
|
775
|
+
} else if (this.checkIdent('anchored')) {
|
|
776
|
+
this.advance()
|
|
777
|
+
const anchor = this.advance().value as 'eyes' | 'feet'
|
|
778
|
+
subcommands.push({ kind: 'anchored', anchor })
|
|
779
|
+
} else if (this.checkIdent('align')) {
|
|
780
|
+
this.advance()
|
|
781
|
+
const axes = this.advance().value
|
|
782
|
+
subcommands.push({ kind: 'align', axes })
|
|
783
|
+
} else if (this.checkIdent('on')) {
|
|
784
|
+
this.advance()
|
|
785
|
+
const relation = this.advance().value
|
|
786
|
+
subcommands.push({ kind: 'on', relation })
|
|
787
|
+
} else if (this.checkIdent('summon')) {
|
|
788
|
+
this.advance()
|
|
789
|
+
const entity = this.advance().value
|
|
790
|
+
subcommands.push({ kind: 'summon', entity })
|
|
791
|
+
} else if (this.checkIdent('store')) {
|
|
792
|
+
this.advance()
|
|
793
|
+
const storeType = this.advance().value // 'result' or 'success'
|
|
794
|
+
if (this.checkIdent('score')) {
|
|
795
|
+
this.advance()
|
|
796
|
+
const target = this.advance().value
|
|
797
|
+
const targetObj = this.advance().value
|
|
798
|
+
if (storeType === 'result') {
|
|
799
|
+
subcommands.push({ kind: 'store_result', target, targetObj })
|
|
800
|
+
} else {
|
|
801
|
+
subcommands.push({ kind: 'store_success', target, targetObj })
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
this.error('store currently only supports score target')
|
|
805
|
+
}
|
|
806
|
+
} else if (this.match('if')) {
|
|
807
|
+
this.parseExecuteCondition(subcommands, 'if')
|
|
808
|
+
} else if (this.match('unless')) {
|
|
809
|
+
this.parseExecuteCondition(subcommands, 'unless')
|
|
755
810
|
} else if (this.match('in')) {
|
|
756
|
-
|
|
811
|
+
// Dimension can be namespaced: minecraft:the_nether
|
|
812
|
+
let dim = this.advance().value
|
|
813
|
+
if (this.match(':')) {
|
|
814
|
+
dim += ':' + this.advance().value
|
|
815
|
+
}
|
|
757
816
|
subcommands.push({ kind: 'in', dimension: dim })
|
|
758
817
|
} else {
|
|
759
|
-
this.error(`Unexpected token in execute statement: ${this.peek().kind}`)
|
|
818
|
+
this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`)
|
|
760
819
|
}
|
|
761
820
|
}
|
|
762
821
|
|
|
@@ -766,6 +825,74 @@ export class Parser {
|
|
|
766
825
|
return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
|
|
767
826
|
}
|
|
768
827
|
|
|
828
|
+
private parseExecuteCondition(subcommands: ExecuteSubcommand[], type: 'if' | 'unless'): void {
|
|
829
|
+
if (this.checkIdent('entity') || this.check('selector')) {
|
|
830
|
+
if (this.checkIdent('entity')) this.advance()
|
|
831
|
+
const selectorOrVar = this.parseSelectorOrVarSelector()
|
|
832
|
+
subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar })
|
|
833
|
+
} else if (this.checkIdent('block')) {
|
|
834
|
+
this.advance()
|
|
835
|
+
const x = this.parseCoordToken()
|
|
836
|
+
const y = this.parseCoordToken()
|
|
837
|
+
const z = this.parseCoordToken()
|
|
838
|
+
const block = this.parseBlockId()
|
|
839
|
+
subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block })
|
|
840
|
+
} else if (this.checkIdent('score')) {
|
|
841
|
+
this.advance()
|
|
842
|
+
const target = this.advance().value
|
|
843
|
+
const targetObj = this.advance().value
|
|
844
|
+
// Check for range or comparison
|
|
845
|
+
if (this.checkIdent('matches')) {
|
|
846
|
+
this.advance()
|
|
847
|
+
const range = this.advance().value
|
|
848
|
+
subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range })
|
|
849
|
+
} else {
|
|
850
|
+
const op = this.advance().value // <, <=, =, >=, >
|
|
851
|
+
const source = this.advance().value
|
|
852
|
+
const sourceObj = this.advance().value
|
|
853
|
+
subcommands.push({
|
|
854
|
+
kind: type === 'if' ? 'if_score' : 'unless_score',
|
|
855
|
+
target, targetObj, op, source, sourceObj
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
this.error(`Unknown condition type after ${type}`)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private parseCoordToken(): string {
|
|
864
|
+
// Handle ~, ^, numbers, relative coords like ~5, ^-3
|
|
865
|
+
const token = this.peek()
|
|
866
|
+
if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
|
|
867
|
+
token.kind === 'int_lit' || token.kind === 'float_lit' ||
|
|
868
|
+
token.kind === '-' || token.kind === 'ident') {
|
|
869
|
+
return this.advance().value
|
|
870
|
+
}
|
|
871
|
+
this.error(`Expected coordinate, got ${token.kind}`)
|
|
872
|
+
return '~'
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private parseBlockId(): string {
|
|
876
|
+
// Parse block ID like minecraft:stone or stone
|
|
877
|
+
let id = this.advance().value
|
|
878
|
+
if (this.match(':')) {
|
|
879
|
+
id += ':' + this.advance().value
|
|
880
|
+
}
|
|
881
|
+
// Handle block states [facing=north]
|
|
882
|
+
if (this.check('[')) {
|
|
883
|
+
id += this.advance().value // [
|
|
884
|
+
while (!this.check(']') && !this.check('eof')) {
|
|
885
|
+
id += this.advance().value
|
|
886
|
+
}
|
|
887
|
+
id += this.advance().value // ]
|
|
888
|
+
}
|
|
889
|
+
return id
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private checkIdent(value: string): boolean {
|
|
893
|
+
return this.check('ident') && this.peek().value === value
|
|
894
|
+
}
|
|
895
|
+
|
|
769
896
|
private parseExprStmt(): Stmt {
|
|
770
897
|
const expr = this.parseExpr()
|
|
771
898
|
this.expect(';')
|