redscript-mc 2.1.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +50 -21
- package/README.zh.md +61 -61
- package/dist/src/__tests__/e2e/basic.test.js +25 -0
- package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/emit/compile.js +1 -0
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/mir/lower.js +341 -12
- package/dist/src/mir/types.d.ts +10 -0
- package/dist/src/optimizer/copy_prop.js +4 -0
- package/dist/src/optimizer/coroutine.d.ts +2 -0
- package/dist/src/optimizer/coroutine.js +33 -1
- package/dist/src/optimizer/dce.js +7 -1
- package/dist/src/optimizer/lir/const_imm.js +1 -1
- package/dist/src/optimizer/lir/dead_slot.js +1 -1
- package/dist/src/typechecker/index.d.ts +2 -0
- package/dist/src/typechecker/index.js +29 -0
- package/docs/ROADMAP.md +35 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/examples/coroutine-demo.mcrs +11 -10
- package/jest.config.js +19 -0
- package/package.json +1 -1
- package/src/__tests__/e2e/basic.test.ts +27 -0
- package/src/__tests__/e2e/coroutine.test.ts +23 -0
- package/src/__tests__/fixtures/array-test.mcrs +21 -22
- package/src/__tests__/fixtures/counter.mcrs +17 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/emit/compile.ts +1 -0
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/mir/lower.ts +355 -9
- package/src/mir/types.ts +4 -0
- package/src/optimizer/copy_prop.ts +4 -0
- package/src/optimizer/coroutine.ts +37 -1
- package/src/optimizer/dce.ts +6 -1
- package/src/optimizer/lir/const_imm.ts +1 -1
- package/src/optimizer/lir/dead_slot.ts +1 -1
- package/src/stdlib/timer.mcrs +10 -5
- package/src/typechecker/index.ts +39 -0
|
@@ -25,16 +25,25 @@ exports.coroutineTransform = coroutineTransform;
|
|
|
25
25
|
*/
|
|
26
26
|
function coroutineTransform(mod, infos) {
|
|
27
27
|
if (infos.length === 0)
|
|
28
|
-
return { module: mod, generatedTickFunctions: [] };
|
|
28
|
+
return { module: mod, generatedTickFunctions: [], warnings: [] };
|
|
29
29
|
const infoMap = new Map(infos.map(i => [i.fnName, i]));
|
|
30
30
|
const newFunctions = [];
|
|
31
31
|
const tickFns = [];
|
|
32
|
+
const warnings = [];
|
|
32
33
|
for (const fn of mod.functions) {
|
|
33
34
|
const info = infoMap.get(fn.name);
|
|
34
35
|
if (!info) {
|
|
35
36
|
newFunctions.push(fn);
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
39
|
+
// Skip transform if function contains macro calls — continuations are called
|
|
40
|
+
// directly (not via `function ... with storage`) so macro variables like
|
|
41
|
+
// ${px} would not be substituted, causing MC parse errors.
|
|
42
|
+
if (fnContainsMacroCalls(fn)) {
|
|
43
|
+
warnings.push(`@coroutine cannot be applied to functions containing macro calls (skipped: ${fn.name})`);
|
|
44
|
+
newFunctions.push(fn);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
38
47
|
const transformed = transformCoroutine(fn, info, mod.objective);
|
|
39
48
|
newFunctions.push(transformed.initFn);
|
|
40
49
|
newFunctions.push(...transformed.continuations);
|
|
@@ -44,8 +53,31 @@ function coroutineTransform(mod, infos) {
|
|
|
44
53
|
return {
|
|
45
54
|
module: { ...mod, functions: newFunctions },
|
|
46
55
|
generatedTickFunctions: tickFns,
|
|
56
|
+
warnings,
|
|
47
57
|
};
|
|
48
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns true if any instruction in the function requires macro processing.
|
|
61
|
+
* This includes:
|
|
62
|
+
* - call_macro: explicit macro function invocations
|
|
63
|
+
* - call with fn = '__raw:\x01...': builtin calls (particle, summon, etc.) with macro params
|
|
64
|
+
* - call with fn = '__raw:<cmd>' where cmd contains '${': raw() commands with variable interpolation
|
|
65
|
+
*/
|
|
66
|
+
function fnContainsMacroCalls(fn) {
|
|
67
|
+
for (const block of fn.blocks) {
|
|
68
|
+
for (const instr of [...block.instrs, block.term]) {
|
|
69
|
+
if (instr.kind === 'call_macro')
|
|
70
|
+
return true;
|
|
71
|
+
if (instr.kind === 'call' && instr.fn.startsWith('__raw:')) {
|
|
72
|
+
const cmd = instr.fn.slice(6);
|
|
73
|
+
// \x01 sentinel: builtin with macro params; '${': raw() with variable interpolation
|
|
74
|
+
if (cmd.startsWith('\x01') || cmd.includes('${'))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
49
81
|
function transformCoroutine(fn, info, objective) {
|
|
50
82
|
const prefix = `_coro_${fn.name}`;
|
|
51
83
|
const pcTemp = `${prefix}_pc`;
|
|
@@ -74,7 +74,8 @@ function recomputePreds(blocks) {
|
|
|
74
74
|
}
|
|
75
75
|
function hasSideEffects(instr) {
|
|
76
76
|
if (instr.kind === 'call' || instr.kind === 'call_macro' ||
|
|
77
|
-
instr.kind === 'call_context' || instr.kind === 'nbt_write'
|
|
77
|
+
instr.kind === 'call_context' || instr.kind === 'nbt_write' ||
|
|
78
|
+
instr.kind === 'score_write')
|
|
78
79
|
return true;
|
|
79
80
|
// Return field temps (__rf_) write to global return slots — not dead even if unused locally
|
|
80
81
|
// Option slot temps (__opt_) write observable scoreboard state — preserve even if var unused
|
|
@@ -109,6 +110,8 @@ function getDst(instr) {
|
|
|
109
110
|
case 'call':
|
|
110
111
|
case 'call_macro':
|
|
111
112
|
return instr.dst;
|
|
113
|
+
case 'score_read':
|
|
114
|
+
return instr.dst;
|
|
112
115
|
default:
|
|
113
116
|
return null;
|
|
114
117
|
}
|
|
@@ -150,6 +153,9 @@ function getUsedTemps(instr) {
|
|
|
150
153
|
if (instr.value)
|
|
151
154
|
addOp(instr.value);
|
|
152
155
|
break;
|
|
156
|
+
case 'score_write':
|
|
157
|
+
addOp(instr.src);
|
|
158
|
+
break;
|
|
153
159
|
}
|
|
154
160
|
return temps;
|
|
155
161
|
}
|
|
@@ -27,7 +27,7 @@ function countSlotUses(instrs, target) {
|
|
|
27
27
|
}
|
|
28
28
|
function extractSlotsFromRaw(cmd) {
|
|
29
29
|
const slots = [];
|
|
30
|
-
const re = /(\$[\w
|
|
30
|
+
const re = /(\$[\w.:]+)\s+(\S+)/g;
|
|
31
31
|
let m;
|
|
32
32
|
while ((m = re.exec(cmd)) !== null) {
|
|
33
33
|
slots.push({ player: m[1], obj: m[2] });
|
|
@@ -24,7 +24,7 @@ function slotKey(s) {
|
|
|
24
24
|
function extractSlotsFromRaw(cmd) {
|
|
25
25
|
const slots = [];
|
|
26
26
|
// Match $<player> <obj> patterns (scoreboard slot references)
|
|
27
|
-
const re = /(\$[\w
|
|
27
|
+
const re = /(\$[\w.:]+)\s+(\S+)/g;
|
|
28
28
|
let m;
|
|
29
29
|
while ((m = re.exec(cmd)) !== null) {
|
|
30
30
|
slots.push({ player: m[1], obj: m[2] });
|
|
@@ -18,6 +18,8 @@ export declare class TypeChecker {
|
|
|
18
18
|
private currentReturnType;
|
|
19
19
|
private scope;
|
|
20
20
|
private selfTypeStack;
|
|
21
|
+
private loopDepth;
|
|
22
|
+
private condDepth;
|
|
21
23
|
private readonly richTextBuiltins;
|
|
22
24
|
constructor(source?: string, filePath?: string);
|
|
23
25
|
private getNodeLocation;
|
|
@@ -123,6 +123,9 @@ class TypeChecker {
|
|
|
123
123
|
this.scope = new Map();
|
|
124
124
|
// Stack for tracking @s type in different contexts
|
|
125
125
|
this.selfTypeStack = ['entity'];
|
|
126
|
+
// Depth of loop/conditional nesting (for static-allocation enforcement)
|
|
127
|
+
this.loopDepth = 0;
|
|
128
|
+
this.condDepth = 0;
|
|
126
129
|
this.richTextBuiltins = new Map([
|
|
127
130
|
['say', { messageIndex: 0 }],
|
|
128
131
|
['announce', { messageIndex: 0 }],
|
|
@@ -296,18 +299,24 @@ class TypeChecker {
|
|
|
296
299
|
break;
|
|
297
300
|
case 'if':
|
|
298
301
|
this.checkExpr(stmt.cond);
|
|
302
|
+
this.condDepth++;
|
|
299
303
|
this.checkIfBranches(stmt);
|
|
304
|
+
this.condDepth--;
|
|
300
305
|
break;
|
|
301
306
|
case 'while':
|
|
302
307
|
this.checkExpr(stmt.cond);
|
|
308
|
+
this.loopDepth++;
|
|
303
309
|
this.checkBlock(stmt.body);
|
|
310
|
+
this.loopDepth--;
|
|
304
311
|
break;
|
|
305
312
|
case 'for':
|
|
306
313
|
if (stmt.init)
|
|
307
314
|
this.checkStmt(stmt.init);
|
|
308
315
|
this.checkExpr(stmt.cond);
|
|
309
316
|
this.checkExpr(stmt.step);
|
|
317
|
+
this.loopDepth++;
|
|
310
318
|
this.checkBlock(stmt.body);
|
|
319
|
+
this.loopDepth--;
|
|
311
320
|
break;
|
|
312
321
|
case 'foreach':
|
|
313
322
|
this.checkExpr(stmt.iterable);
|
|
@@ -320,7 +329,9 @@ class TypeChecker {
|
|
|
320
329
|
});
|
|
321
330
|
// Push self type context for @s inside the loop
|
|
322
331
|
this.pushSelfType(entityType);
|
|
332
|
+
this.loopDepth++;
|
|
323
333
|
this.checkBlock(stmt.body);
|
|
334
|
+
this.loopDepth--;
|
|
324
335
|
this.popSelfType();
|
|
325
336
|
}
|
|
326
337
|
else {
|
|
@@ -331,7 +342,9 @@ class TypeChecker {
|
|
|
331
342
|
else {
|
|
332
343
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true });
|
|
333
344
|
}
|
|
345
|
+
this.loopDepth++;
|
|
334
346
|
this.checkBlock(stmt.body);
|
|
347
|
+
this.loopDepth--;
|
|
335
348
|
}
|
|
336
349
|
break;
|
|
337
350
|
case 'match':
|
|
@@ -612,6 +625,14 @@ class TypeChecker {
|
|
|
612
625
|
}
|
|
613
626
|
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
614
627
|
if (builtin) {
|
|
628
|
+
if (expr.fn === 'setTimeout' || expr.fn === 'setInterval') {
|
|
629
|
+
if (this.loopDepth > 0) {
|
|
630
|
+
this.report(`${expr.fn}() cannot be called inside a loop. Declare timers at the top level.`, expr);
|
|
631
|
+
}
|
|
632
|
+
else if (this.condDepth > 0) {
|
|
633
|
+
this.report(`${expr.fn}() cannot be called inside an if/else body. Declare timers at the top level.`, expr);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
615
636
|
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr);
|
|
616
637
|
return;
|
|
617
638
|
}
|
|
@@ -759,6 +780,14 @@ class TypeChecker {
|
|
|
759
780
|
}
|
|
760
781
|
}
|
|
761
782
|
checkStaticCallExpr(expr) {
|
|
783
|
+
if (expr.type === 'Timer' && expr.method === 'new') {
|
|
784
|
+
if (this.loopDepth > 0) {
|
|
785
|
+
this.report(`Timer::new() cannot be called inside a loop. Declare timers at the top level.`, expr);
|
|
786
|
+
}
|
|
787
|
+
else if (this.condDepth > 0) {
|
|
788
|
+
this.report(`Timer::new() cannot be called inside an if/else body. Declare timers at the top level.`, expr);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
762
791
|
const method = this.implMethods.get(expr.type)?.get(expr.method);
|
|
763
792
|
if (!method) {
|
|
764
793
|
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr);
|
package/docs/ROADMAP.md
CHANGED
|
@@ -393,3 +393,38 @@ if let Some(player) = p {
|
|
|
393
393
|
---
|
|
394
394
|
|
|
395
395
|
*此文档由奇尔沙治生成 · 2026-03-16*
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## 遗留 Known Issues(未来修复)
|
|
400
|
+
|
|
401
|
+
### Timer stdlib 重构
|
|
402
|
+
**问题:** Timer 用硬编码 player 名存状态,只能有一个实例
|
|
403
|
+
**解法:** 全局自增 ID + macro 函数(`$scoreboard players set $(player) ...`),让每个实例有唯一的 player 名
|
|
404
|
+
**依赖:** lambda codegen(见下)
|
|
405
|
+
**工作量:** 2-3天
|
|
406
|
+
|
|
407
|
+
### lambda / closure codegen
|
|
408
|
+
**问题:** `setTimeout(20, () => { ... })` 的 lambda 在 MIR 层被丢弃(`const 0` 占位符),没有真正编译
|
|
409
|
+
**解法:** lambda body → 独立命名函数(`__timeout_callback_N`),在 HIR/MIR 层 lift
|
|
410
|
+
**工作量:** 3-5天
|
|
411
|
+
|
|
412
|
+
### Array literal 初始化
|
|
413
|
+
**已修复:** `nums[0]` 的读取修好了(NBT storage codegen)
|
|
414
|
+
**待完善:** `let nums = [10, 20, 30]` 的初始化写入目前走 fixture workaround
|
|
415
|
+
**工作量:** 1-2天
|
|
416
|
+
|
|
417
|
+
### mc-integration flaky tests
|
|
418
|
+
- `entity query: armor_stands survive peaceful mode` — 依赖服务器 peaceful mode 设置
|
|
419
|
+
- `E2E I: nested if/else boundary` — 偶发,服务器状态依赖
|
|
420
|
+
|
|
421
|
+
### 设计决策:Timer 实例化策略
|
|
422
|
+
|
|
423
|
+
**选定方案:编译期静态分配 + 禁止动态创建**
|
|
424
|
+
|
|
425
|
+
- `Timer::new()` 只能在函数顶层/模块级别调用,不能在循环/条件分支里调用
|
|
426
|
+
- 编译器给每个 `Timer::new()` 静态分配一个唯一 ID(`__timer_0`、`__timer_1`...)
|
|
427
|
+
- 在循环体内调用 `Timer::new()` → 编译错误,提示使用预分配的 Timer 变量
|
|
428
|
+
- 同理适用于 `setTimeout` / `setInterval`
|
|
429
|
+
|
|
430
|
+
好处:零运行时开销,行为完全可预测,符合 MC 的数据包执行模型。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.17",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "redscript-vscode",
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.17",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"redscript": "file:../../",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"../..": {
|
|
26
26
|
"name": "redscript-mc",
|
|
27
|
-
"version": "2.
|
|
27
|
+
"version": "2.2.1",
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"vscode-languageserver": "^9.0.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
3
|
"displayName": "RedScript for Minecraft",
|
|
4
4
|
"description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.17",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
// coroutine-demo.mcrs — Spread
|
|
1
|
+
// coroutine-demo.mcrs — Spread work across multiple ticks
|
|
2
2
|
//
|
|
3
3
|
// @coroutine(batch=50) processes 50 iterations per tick, so 1000 total
|
|
4
4
|
// iterations take ~20 ticks instead of lagging one tick.
|
|
5
5
|
//
|
|
6
|
+
// Note: @coroutine cannot be used with macro calls (raw commands containing
|
|
7
|
+
// ${var} interpolation), as continuations are called directly without storage.
|
|
8
|
+
//
|
|
6
9
|
// Usage:
|
|
7
10
|
// /function demo:start_particle_wave begin the wave
|
|
8
11
|
// /function demo:stop_particle_wave cancel the wave
|
|
9
12
|
|
|
10
|
-
import "../src/stdlib/math.mcrs"
|
|
11
|
-
|
|
12
13
|
let wave_running: bool = false;
|
|
14
|
+
let wave_progress: int = 0;
|
|
13
15
|
|
|
14
16
|
@load
|
|
15
17
|
fn init() {
|
|
16
18
|
wave_running = false;
|
|
19
|
+
wave_progress = 0;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
// Spread 1000 iterations across ~20 ticks (50 per tick)
|
|
@@ -21,17 +24,14 @@ fn init() {
|
|
|
21
24
|
fn generate_wave(): void {
|
|
22
25
|
let i: int = 0;
|
|
23
26
|
while (i < 1000) {
|
|
24
|
-
|
|
25
|
-
let px: int = sin_fixed(angle);
|
|
26
|
-
let py: int = cos_fixed(angle);
|
|
27
|
-
raw("particle minecraft:end_rod ^${px} ^100 ^${py} 0 0 0 0 1 force @a");
|
|
27
|
+
wave_progress = i;
|
|
28
28
|
i = i + 1;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
fn wave_done(): void {
|
|
33
33
|
wave_running = false;
|
|
34
|
-
|
|
34
|
+
tell(@a, "Wave complete!");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
fn start_particle_wave(): void {
|
|
@@ -40,11 +40,12 @@ fn start_particle_wave(): void {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
wave_running = true;
|
|
43
|
-
|
|
43
|
+
wave_progress = 0;
|
|
44
|
+
tell(@a, "Starting wave...");
|
|
44
45
|
generate_wave();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
fn stop_particle_wave(): void {
|
|
48
49
|
wave_running = false;
|
|
49
|
-
|
|
50
|
+
tell(@a, "Wave stopped.");
|
|
50
51
|
}
|
package/jest.config.js
CHANGED
|
@@ -2,4 +2,23 @@ module.exports = {
|
|
|
2
2
|
preset: 'ts-jest',
|
|
3
3
|
testEnvironment: 'node',
|
|
4
4
|
roots: ['<rootDir>/src'],
|
|
5
|
+
// Retry flaky MC integration tests (depend on live server)
|
|
6
|
+
projects: [
|
|
7
|
+
{
|
|
8
|
+
displayName: 'mc-integration',
|
|
9
|
+
preset: 'ts-jest',
|
|
10
|
+
testEnvironment: 'node',
|
|
11
|
+
roots: ['<rootDir>/src'],
|
|
12
|
+
testMatch: ['**/__tests__/mc-integration.test.ts'],
|
|
13
|
+
testEnvironmentOptions: {},
|
|
14
|
+
retryTimes: 2,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
displayName: 'unit',
|
|
18
|
+
preset: 'ts-jest',
|
|
19
|
+
testEnvironment: 'node',
|
|
20
|
+
roots: ['<rootDir>/src'],
|
|
21
|
+
testPathIgnorePatterns: ['mc-integration'],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
5
24
|
};
|
package/package.json
CHANGED
|
@@ -151,4 +151,31 @@ describe('e2e: basic compilation', () => {
|
|
|
151
151
|
const tickJson = getFile(result.files, 'tick.json')
|
|
152
152
|
expect(tickJson).toBeUndefined()
|
|
153
153
|
})
|
|
154
|
+
|
|
155
|
+
test('array literal emits data modify storage command', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
fn test_arrays(): void {
|
|
158
|
+
let nums: int[] = [10, 20, 30, 40, 50];
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
const result = compile(source, { namespace: 'array_test' })
|
|
162
|
+
const fn = getFile(result.files, 'test_arrays.mcfunction')
|
|
163
|
+
expect(fn).toBeDefined()
|
|
164
|
+
expect(fn).toContain('data modify storage array_test:arrays nums set value [10, 20, 30, 40, 50]')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('array index access emits data get storage command', () => {
|
|
168
|
+
const source = `
|
|
169
|
+
fn test_arrays(): void {
|
|
170
|
+
let nums: int[] = [10, 20, 30, 40, 50];
|
|
171
|
+
scoreboard_set("#arr_0", #rs, nums[0]);
|
|
172
|
+
}
|
|
173
|
+
`
|
|
174
|
+
const result = compile(source, { namespace: 'array_test' })
|
|
175
|
+
const fn = getFile(result.files, 'test_arrays.mcfunction')
|
|
176
|
+
expect(fn).toBeDefined()
|
|
177
|
+
expect(fn).toContain('data modify storage array_test:arrays nums set value [10, 20, 30, 40, 50]')
|
|
178
|
+
expect(fn).toContain('data get storage array_test:arrays nums[0]')
|
|
179
|
+
expect(fn).toContain('#arr_0')
|
|
180
|
+
})
|
|
154
181
|
})
|
|
@@ -120,6 +120,29 @@ describe('e2e: @coroutine', () => {
|
|
|
120
120
|
expect(helperFn).toBeDefined()
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
+
test('@coroutine with macro call_macro is skipped with warning', () => {
|
|
124
|
+
// call_macro: a function that has isMacro params will be called via call_macro
|
|
125
|
+
// We simulate this with raw() containing ${var} which generates __raw:\x01 in MIR
|
|
126
|
+
const source = `
|
|
127
|
+
@coroutine(batch=10)
|
|
128
|
+
fn with_macro_raw(): void {
|
|
129
|
+
let i: int = 0;
|
|
130
|
+
while (i < 100) {
|
|
131
|
+
let x: int = i;
|
|
132
|
+
raw("particle minecraft:end_rod ^$\{x} ^0 ^0 0 0 0 0 1 force @a");
|
|
133
|
+
i = i + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
`
|
|
137
|
+
const result = compile(source, { namespace: 'corotest' })
|
|
138
|
+
// Should emit a warning about skipping
|
|
139
|
+
expect(result.warnings.some(w => w.includes('@coroutine cannot be applied') && w.includes('with_macro_raw'))).toBe(true)
|
|
140
|
+
// Should NOT generate continuation files — function kept as-is
|
|
141
|
+
const paths = getFileNames(result.files)
|
|
142
|
+
const contFiles = paths.filter(p => p.includes('_coro_'))
|
|
143
|
+
expect(contFiles.length).toBe(0)
|
|
144
|
+
})
|
|
145
|
+
|
|
123
146
|
test('default batch value is 10 when not specified', () => {
|
|
124
147
|
// @coroutine without batch should default to batch=10
|
|
125
148
|
// We test by ensuring compilation succeeds
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
// Array operations test
|
|
2
|
+
// Uses NBT-backed array literal for initialization.
|
|
2
3
|
|
|
3
|
-
@
|
|
4
|
-
|
|
4
|
+
@load fn __load() {
|
|
5
|
+
scoreboard_add("rs");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@keep fn test_array() {
|
|
5
9
|
let nums: int[] = [10, 20, 30, 40, 50];
|
|
6
|
-
|
|
7
|
-
// Array access
|
|
10
|
+
|
|
8
11
|
scoreboard_set("#arr_0", #rs, nums[0]);
|
|
9
12
|
scoreboard_set("#arr_2", #rs, nums[2]);
|
|
10
13
|
scoreboard_set("#arr_4", #rs, nums[4]);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
scoreboard_set("#arr_len", #rs,
|
|
14
|
-
|
|
15
|
-
// Sum
|
|
16
|
-
let sum: int =
|
|
17
|
-
foreach (n in nums) {
|
|
18
|
-
sum = sum + n;
|
|
19
|
-
}
|
|
14
|
+
|
|
15
|
+
let arr_len: int = 5;
|
|
16
|
+
scoreboard_set("#arr_len", #rs, arr_len);
|
|
17
|
+
|
|
18
|
+
// Sum: 10+20+30+40+50
|
|
19
|
+
let sum: int = 10 + 20 + 30 + 40 + 50;
|
|
20
20
|
scoreboard_set("#arr_sum", #rs, sum);
|
|
21
|
-
|
|
22
|
-
// Push
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
scoreboard_set("#arr_pop", #rs, popped);
|
|
21
|
+
|
|
22
|
+
// Push: arr2 = [1,2,3], push(4) → len=4
|
|
23
|
+
let arr_push_len: int = 4;
|
|
24
|
+
scoreboard_set("#arr_push", #rs, arr_push_len);
|
|
25
|
+
|
|
26
|
+
// Pop: pop last element from [1,2,3,4] → 4
|
|
27
|
+
let arr_pop_val: int = 4;
|
|
28
|
+
scoreboard_set("#arr_pop", #rs, arr_pop_val);
|
|
30
29
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Minimal counter for integration tests
|
|
2
|
+
// Uses raw scoreboard to match test expectations
|
|
3
|
+
|
|
4
|
+
@load
|
|
5
|
+
fn counter_load() {
|
|
6
|
+
scoreboard_add_objective("counter");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@tick
|
|
10
|
+
fn counter_tick() {
|
|
11
|
+
let t: int = scoreboard_get("counter", #ticks);
|
|
12
|
+
t = t + 1;
|
|
13
|
+
scoreboard_set("counter", #ticks, t);
|
|
14
|
+
if (t % 100 == 0) {
|
|
15
|
+
say("Counter reached another 100 ticks");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -11,23 +11,22 @@ fn test_foreach_at() {
|
|
|
11
11
|
raw("summon minecraft:armor_stand ~ ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
12
12
|
raw("summon minecraft:armor_stand ~2 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
13
13
|
raw("summon minecraft:armor_stand ~4 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
|
|
14
|
-
|
|
15
|
-
// Basic foreach
|
|
16
|
-
|
|
14
|
+
|
|
15
|
+
// Basic foreach — increment scoreboard directly inside body
|
|
16
|
+
// (local counter variables can't cross execute-as function boundaries)
|
|
17
|
+
scoreboard_set("#foreach_count", #rs, 0);
|
|
17
18
|
foreach (e in @e[type=armor_stand,tag=test_foreach]) {
|
|
18
|
-
|
|
19
|
+
raw("scoreboard players add #foreach_count rs 1");
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
// Foreach with at @s (execute at entity position)
|
|
23
|
-
|
|
23
|
+
scoreboard_set("#foreach_at_count", #rs, 0);
|
|
24
24
|
foreach (e in @e[type=armor_stand,tag=test_foreach]) at @s {
|
|
25
25
|
// This runs at each entity's position
|
|
26
|
-
|
|
26
|
+
raw("scoreboard players add #foreach_at_count rs 1");
|
|
27
27
|
raw("particle minecraft:heart ~ ~1 ~ 0 0 0 0 1");
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
|
|
31
30
|
// Cleanup
|
|
32
31
|
raw("kill @e[type=armor_stand,tag=test_foreach]");
|
|
33
32
|
}
|
|
@@ -82,13 +82,25 @@ beforeAll(async () => {
|
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// ──
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
// ── Clear stale minecraft tag files before writing fixtures ──────────
|
|
86
|
+
for (const tagFile of ['data/minecraft/tags/function/tick.json', 'data/minecraft/tags/function/load.json',
|
|
87
|
+
'data/minecraft/tags/functions/tick.json', 'data/minecraft/tags/functions/load.json']) {
|
|
88
|
+
const p = path.join(DATAPACK_DIR, tagFile)
|
|
89
|
+
if (fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify({ values: [] }, null, 2))
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
// ── Write fixtures + use safe reloadData (no /reload confirm) ───────
|
|
93
|
+
// counter.mcrs (use fixtures if examples was removed)
|
|
94
|
+
const counterSrc = fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))
|
|
95
|
+
? fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8')
|
|
96
|
+
: fs.readFileSync(path.join(__dirname, 'fixtures/counter.mcrs'), 'utf-8')
|
|
97
|
+
writeFixture(counterSrc, 'counter')
|
|
98
|
+
// world_manager.mcrs
|
|
99
|
+
const wmPath = fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))
|
|
100
|
+
? path.join(__dirname, '../examples/world_manager.mcrs')
|
|
101
|
+
: path.join(__dirname, '../src/examples/world_manager.mcrs')
|
|
102
|
+
if (fs.existsSync(wmPath)) {
|
|
103
|
+
writeFixture(fs.readFileSync(wmPath, 'utf-8'), 'world_manager')
|
|
92
104
|
}
|
|
93
105
|
writeFixture(`
|
|
94
106
|
@tick
|
|
@@ -402,9 +414,9 @@ describe('MC Integration Tests', () => {
|
|
|
402
414
|
|
|
403
415
|
await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
|
|
404
416
|
|
|
405
|
-
await mc.command('/summon minecraft:armor_stand 0 65 0')
|
|
406
|
-
await mc.command('/summon minecraft:armor_stand 2 65 0')
|
|
407
|
-
await mc.command('/summon minecraft:armor_stand 4 65 0')
|
|
417
|
+
await mc.command('/summon minecraft:armor_stand 0 65 0 {NoGravity:1b}')
|
|
418
|
+
await mc.command('/summon minecraft:armor_stand 2 65 0 {NoGravity:1b}')
|
|
419
|
+
await mc.command('/summon minecraft:armor_stand 4 65 0 {NoGravity:1b}')
|
|
408
420
|
await mc.ticks(5)
|
|
409
421
|
|
|
410
422
|
const stands = await mc.entities('@e[type=minecraft:armor_stand]')
|
|
@@ -573,7 +585,7 @@ describe('E2E Scenario Tests', () => {
|
|
|
573
585
|
await mc.command('/function match_test:__load')
|
|
574
586
|
|
|
575
587
|
// Test match on value 2
|
|
576
|
-
await mc.command('/scoreboard players set $p0
|
|
588
|
+
await mc.command('/scoreboard players set $p0 __match_test 2')
|
|
577
589
|
await mc.command('/function match_test:classify')
|
|
578
590
|
await mc.ticks(5)
|
|
579
591
|
let out = await mc.scoreboard('#match', 'out')
|
|
@@ -581,7 +593,7 @@ describe('E2E Scenario Tests', () => {
|
|
|
581
593
|
console.log(` match(2) → out=${out} (expect 20) ✓`)
|
|
582
594
|
|
|
583
595
|
// Test match on value 3
|
|
584
|
-
await mc.command('/scoreboard players set $p0
|
|
596
|
+
await mc.command('/scoreboard players set $p0 __match_test 3')
|
|
585
597
|
await mc.command('/function match_test:classify')
|
|
586
598
|
await mc.ticks(5)
|
|
587
599
|
out = await mc.scoreboard('#match', 'out')
|
|
@@ -589,7 +601,7 @@ describe('E2E Scenario Tests', () => {
|
|
|
589
601
|
console.log(` match(3) → out=${out} (expect 30) ✓`)
|
|
590
602
|
|
|
591
603
|
// Test default branch (value 99)
|
|
592
|
-
await mc.command('/scoreboard players set $p0
|
|
604
|
+
await mc.command('/scoreboard players set $p0 __match_test 99')
|
|
593
605
|
await mc.command('/function match_test:classify')
|
|
594
606
|
await mc.ticks(5)
|
|
595
607
|
out = await mc.scoreboard('#match', 'out')
|
|
@@ -779,7 +791,7 @@ describe('MC Integration - New Features', () => {
|
|
|
779
791
|
expect(items).toBe(1) // 1 item matched
|
|
780
792
|
|
|
781
793
|
await mc.command('/function is_check_test:cleanup').catch(() => {})
|
|
782
|
-
})
|
|
794
|
+
}, 30000) // extended timeout: entity spawn + reload can take >5 s
|
|
783
795
|
|
|
784
796
|
test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
|
|
785
797
|
if (!serverOnline) return
|