redscript-mc 2.1.0 → 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 +86 -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__/lsp.test.js +76 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/mc-syntax.test.js +1 -6
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/stdlib-include.test.d.ts +1 -0
- package/dist/src/__tests__/stdlib-include.test.js +86 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/cli.js +10 -3
- package/dist/src/compile.d.ts +1 -0
- package/dist/src/compile.js +33 -10
- package/dist/src/emit/compile.d.ts +2 -0
- package/dist/src/emit/compile.js +3 -2
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/lsp/server.js +51 -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/editors/vscode/syntaxes/redscript.tmLanguage.json +34 -0
- package/examples/coroutine-demo.mcrs +51 -0
- package/examples/enum-demo.mcrs +95 -0
- package/examples/scheduler-demo.mcrs +59 -0
- 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__/lsp.test.ts +89 -0
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/mc-syntax.test.ts +1 -7
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/stdlib-include.test.ts +61 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/cli.ts +9 -1
- package/src/compile.ts +44 -15
- package/src/emit/compile.ts +5 -2
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/lsp/server.ts +55 -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
- package/examples/spiral.mcrs +0 -43
- package/src/examples/arena.mcrs +0 -44
- package/src/examples/counter.mcrs +0 -12
- package/src/examples/new_features_demo.mcrs +0 -193
- package/src/examples/rpg.mcrs +0 -13
- package/src/examples/stdlib_demo.mcrs +0 -181
|
@@ -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.1
|
|
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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"patterns": [
|
|
9
9
|
{ "include": "#comments" },
|
|
10
10
|
{ "include": "#decorators" },
|
|
11
|
+
{ "include": "#fstring" },
|
|
11
12
|
{ "include": "#string-interpolation" },
|
|
12
13
|
{ "include": "#strings" },
|
|
13
14
|
{ "include": "#keywords" },
|
|
@@ -91,6 +92,39 @@
|
|
|
91
92
|
]
|
|
92
93
|
},
|
|
93
94
|
|
|
95
|
+
"fstring": {
|
|
96
|
+
"comment": "f-string: f\"Hello {name}\" — f prefix gets special color, {expr} highlighted",
|
|
97
|
+
"begin": "(f)(\")",
|
|
98
|
+
"beginCaptures": {
|
|
99
|
+
"1": { "name": "storage.type.string.redscript" },
|
|
100
|
+
"2": { "name": "punctuation.definition.string.begin.redscript" }
|
|
101
|
+
},
|
|
102
|
+
"end": "\"",
|
|
103
|
+
"endCaptures": {
|
|
104
|
+
"0": { "name": "punctuation.definition.string.end.redscript" }
|
|
105
|
+
},
|
|
106
|
+
"name": "string.interpolated.redscript",
|
|
107
|
+
"patterns": [
|
|
108
|
+
{
|
|
109
|
+
"begin": "\\{",
|
|
110
|
+
"end": "\\}",
|
|
111
|
+
"beginCaptures": { "0": { "name": "punctuation.definition.interpolation.begin.redscript" } },
|
|
112
|
+
"endCaptures": { "0": { "name": "punctuation.definition.interpolation.end.redscript" } },
|
|
113
|
+
"contentName": "meta.interpolation.redscript",
|
|
114
|
+
"patterns": [
|
|
115
|
+
{ "include": "#selectors" },
|
|
116
|
+
{ "include": "#numbers" },
|
|
117
|
+
{ "include": "#fn-call" },
|
|
118
|
+
{
|
|
119
|
+
"name": "variable.other.redscript",
|
|
120
|
+
"match": "[a-zA-Z_][a-zA-Z0-9_]*"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{ "name": "constant.character.escape.redscript", "match": "\\\\." }
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
|
|
94
128
|
"string-interpolation": {
|
|
95
129
|
"comment": "Interpolated string with ${expr} — highlight the expression inside",
|
|
96
130
|
"begin": "\"(?=[^\"]*\\$\\{)",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// coroutine-demo.mcrs — Spread work across multiple ticks
|
|
2
|
+
//
|
|
3
|
+
// @coroutine(batch=50) processes 50 iterations per tick, so 1000 total
|
|
4
|
+
// iterations take ~20 ticks instead of lagging one tick.
|
|
5
|
+
//
|
|
6
|
+
// Note: @coroutine cannot be used with macro calls (raw commands containing
|
|
7
|
+
// ${var} interpolation), as continuations are called directly without storage.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// /function demo:start_particle_wave begin the wave
|
|
11
|
+
// /function demo:stop_particle_wave cancel the wave
|
|
12
|
+
|
|
13
|
+
let wave_running: bool = false;
|
|
14
|
+
let wave_progress: int = 0;
|
|
15
|
+
|
|
16
|
+
@load
|
|
17
|
+
fn init() {
|
|
18
|
+
wave_running = false;
|
|
19
|
+
wave_progress = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Spread 1000 iterations across ~20 ticks (50 per tick)
|
|
23
|
+
@coroutine(batch=50, onDone=wave_done)
|
|
24
|
+
fn generate_wave(): void {
|
|
25
|
+
let i: int = 0;
|
|
26
|
+
while (i < 1000) {
|
|
27
|
+
wave_progress = i;
|
|
28
|
+
i = i + 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn wave_done(): void {
|
|
33
|
+
wave_running = false;
|
|
34
|
+
tell(@a, "Wave complete!");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn start_particle_wave(): void {
|
|
38
|
+
if (wave_running) {
|
|
39
|
+
tell(@a, "Wave already running.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
wave_running = true;
|
|
43
|
+
wave_progress = 0;
|
|
44
|
+
tell(@a, "Starting wave...");
|
|
45
|
+
generate_wave();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn stop_particle_wave(): void {
|
|
49
|
+
wave_running = false;
|
|
50
|
+
tell(@a, "Wave stopped.");
|
|
51
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// enum-demo.mcrs — NPC AI state machine using enums and match
|
|
2
|
+
//
|
|
3
|
+
// An NPC cycles through Idle → Moving → Attacking states.
|
|
4
|
+
// Each state has its own behaviour, driven by @tick.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// /function demo:npc_start activate the NPC AI
|
|
8
|
+
// /function demo:npc_stop deactivate the NPC AI
|
|
9
|
+
|
|
10
|
+
enum Phase {
|
|
11
|
+
Idle, // 0 — waiting for a player nearby
|
|
12
|
+
Moving, // 1 — closing the distance
|
|
13
|
+
Attacking, // 2 — striking the nearest player
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct NpcState {
|
|
17
|
+
phase: int, // current Phase value
|
|
18
|
+
ticks: int, // ticks in the current phase
|
|
19
|
+
active: bool
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let npc: NpcState = {
|
|
23
|
+
phase: 0,
|
|
24
|
+
ticks: 0,
|
|
25
|
+
active: false
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
@load
|
|
29
|
+
fn npc_load() {
|
|
30
|
+
npc.phase = Phase.Idle;
|
|
31
|
+
npc.ticks = 0;
|
|
32
|
+
npc.active = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn npc_start() {
|
|
36
|
+
npc.active = true;
|
|
37
|
+
npc.phase = Phase.Idle;
|
|
38
|
+
npc.ticks = 0;
|
|
39
|
+
actionbar(@a, "[NPC] AI activated");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn npc_stop() {
|
|
43
|
+
npc.active = false;
|
|
44
|
+
actionbar(@a, "[NPC] AI deactivated");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Phase handlers ────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
fn phase_idle() {
|
|
50
|
+
actionbar(@a, "[NPC] Idle — scanning for targets...");
|
|
51
|
+
// After 40 ticks (2 seconds) transition to Moving
|
|
52
|
+
if (npc.ticks >= 40) {
|
|
53
|
+
npc.phase = Phase.Moving;
|
|
54
|
+
npc.ticks = 0;
|
|
55
|
+
title(@a, "NPC begins moving");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn phase_moving() {
|
|
60
|
+
actionbar(@a, "[NPC] Moving — closing distance");
|
|
61
|
+
// Simulate movement toward nearest player
|
|
62
|
+
raw("execute as @e[type=minecraft:zombie,tag=npc_ai] at @s run tp @s @p[limit=1] 0 0 0");
|
|
63
|
+
if (npc.ticks >= 60) {
|
|
64
|
+
npc.phase = Phase.Attacking;
|
|
65
|
+
npc.ticks = 0;
|
|
66
|
+
title(@a, "NPC attacks!");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn phase_attacking() {
|
|
71
|
+
actionbar(@a, "[NPC] Attacking!");
|
|
72
|
+
raw("execute as @e[type=minecraft:zombie,tag=npc_ai] at @s run effect give @p[limit=1,distance=..3] minecraft:slowness 1 1 true");
|
|
73
|
+
if (npc.ticks >= 30) {
|
|
74
|
+
npc.phase = Phase.Idle;
|
|
75
|
+
npc.ticks = 0;
|
|
76
|
+
title(@a, "NPC backs off");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Main tick ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
@tick
|
|
83
|
+
fn npc_tick() {
|
|
84
|
+
if (!npc.active) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
npc.ticks = npc.ticks + 1;
|
|
89
|
+
|
|
90
|
+
match (npc.phase) {
|
|
91
|
+
Phase.Idle => { phase_idle(); }
|
|
92
|
+
Phase.Moving => { phase_moving(); }
|
|
93
|
+
Phase.Attacking => { phase_attacking(); }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// scheduler-demo.mcrs — Delayed event triggering with @schedule
|
|
2
|
+
//
|
|
3
|
+
// @schedule(ticks=N) generates a _schedule_xxx wrapper that emits
|
|
4
|
+
// `schedule function ns:xxx Nt`, deferring execution by N ticks.
|
|
5
|
+
//
|
|
6
|
+
// 20 ticks = 1 second in Minecraft.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// /function demo:begin_countdown trigger the 1-second delayed reward
|
|
10
|
+
// /function demo:announce_morning schedule a sunrise announcement
|
|
11
|
+
|
|
12
|
+
// ── 1-second delayed reward ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
fn begin_countdown() {
|
|
15
|
+
title(@a, "Get ready...");
|
|
16
|
+
raw("function demo:_schedule_reward_players");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Called automatically 1 second (20t) after _schedule_reward_players fires
|
|
20
|
+
@schedule(ticks=20)
|
|
21
|
+
fn reward_players(): void {
|
|
22
|
+
title(@a, "Go!");
|
|
23
|
+
raw("effect give @a minecraft:speed 5 1 true");
|
|
24
|
+
raw("effect give @a minecraft:jump_boost 5 1 true");
|
|
25
|
+
tell(@a, "Speed and Jump Boost applied for 5 seconds.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── 5-second delayed announcement ────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
fn announce_morning() {
|
|
31
|
+
tell(@a, "Sunrise in 5 seconds...");
|
|
32
|
+
raw("function demo:_schedule_sunrise_event");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@schedule(ticks=100)
|
|
36
|
+
fn sunrise_event(): void {
|
|
37
|
+
raw("time set day");
|
|
38
|
+
raw("weather clear");
|
|
39
|
+
title(@a, "Good morning!");
|
|
40
|
+
subtitle(@a, "A new day begins");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Chain: schedule a follow-up from inside a scheduled function ──────────
|
|
44
|
+
|
|
45
|
+
fn start_chain() {
|
|
46
|
+
tell(@a, "Chain started — phase 1 runs in 1s, phase 2 in 3s total.");
|
|
47
|
+
raw("function demo:_schedule_chain_phase1");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@schedule(ticks=20)
|
|
51
|
+
fn chain_phase1(): void {
|
|
52
|
+
actionbar(@a, "Phase 1 complete");
|
|
53
|
+
raw("function demo:_schedule_chain_phase2");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@schedule(ticks=40)
|
|
57
|
+
fn chain_phase2(): void {
|
|
58
|
+
actionbar(@a, "Phase 2 complete — chain done!");
|
|
59
|
+
}
|
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
|
}
|