redscript-mc 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +10 -10
- package/dist/__tests__/codegen.test.js +1 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +146 -5
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lowering.test.js +36 -3
- package/dist/__tests__/mc-integration.test.js +255 -10
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/runtime.test.js +1 -1
- package/dist/ast/types.d.ts +21 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +8 -2
- package/dist/codegen/structure/index.js +7 -1
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +7 -2
- package/dist/ir/types.js +1 -1
- package/dist/lowering/index.d.ts +2 -0
- package/dist/lowering/index.js +183 -8
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +75 -7
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +152 -9
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +10 -10
- package/src/__tests__/codegen.test.ts +1 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +134 -5
- package/src/__tests__/lowering.test.ts +48 -3
- package/src/__tests__/mc-integration.test.ts +285 -10
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/runtime.test.ts +1 -1
- package/src/ast/types.ts +20 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +9 -2
- package/src/codegen/structure/index.ts +8 -1
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +8 -2
- package/src/lowering/index.ts +156 -8
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +81 -8
- package/src/stdlib/README.md +155 -147
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
|
@@ -65,12 +65,12 @@ beforeAll(async () => {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ── Write fixtures + use safe reloadData (no /reload confirm) ───────
|
|
68
|
-
// counter.
|
|
69
|
-
if (fs.existsSync(path.join(__dirname, '../examples/counter.
|
|
70
|
-
writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.
|
|
68
|
+
// counter.mcrs
|
|
69
|
+
if (fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))) {
|
|
70
|
+
writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8'), 'counter')
|
|
71
71
|
}
|
|
72
|
-
if (fs.existsSync(path.join(__dirname, '../examples/world_manager.
|
|
73
|
-
writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.
|
|
72
|
+
if (fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))) {
|
|
73
|
+
writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.mcrs'), 'utf-8'), 'world_manager')
|
|
74
74
|
}
|
|
75
75
|
writeFixture(`
|
|
76
76
|
@tick
|
|
@@ -154,12 +154,118 @@ beforeAll(async () => {
|
|
|
154
154
|
}
|
|
155
155
|
`, 'fill_test')
|
|
156
156
|
|
|
157
|
+
// Scenario E: for-range loop — loop counter increments exactly N times
|
|
158
|
+
writeFixture(`
|
|
159
|
+
fn count_to_five() {
|
|
160
|
+
scoreboard_set("#range", "counter", 0);
|
|
161
|
+
for i in 0..5 {
|
|
162
|
+
let c: int = scoreboard_get("#range", "counter");
|
|
163
|
+
scoreboard_set("#range", "counter", c + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
`, 'range_test')
|
|
167
|
+
|
|
168
|
+
// Scenario F: function call with return value — verifies $ret propagation
|
|
169
|
+
writeFixture(`
|
|
170
|
+
fn triple(x: int) -> int {
|
|
171
|
+
return x * 3;
|
|
172
|
+
}
|
|
173
|
+
fn run_nested() {
|
|
174
|
+
let a: int = triple(4);
|
|
175
|
+
scoreboard_set("#nested", "result", a);
|
|
176
|
+
}
|
|
177
|
+
`, 'nested_test')
|
|
178
|
+
|
|
179
|
+
// Scenario G: match statement dispatches to correct branch
|
|
180
|
+
writeFixture(`
|
|
181
|
+
fn classify(x: int) {
|
|
182
|
+
match (x) {
|
|
183
|
+
1 => { scoreboard_set("#match", "out", 10); }
|
|
184
|
+
2 => { scoreboard_set("#match", "out", 20); }
|
|
185
|
+
3 => { scoreboard_set("#match", "out", 30); }
|
|
186
|
+
_ => { scoreboard_set("#match", "out", -1); }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
`, 'match_test')
|
|
190
|
+
|
|
191
|
+
// Scenario H: while loop counts down
|
|
192
|
+
writeFixture(`
|
|
193
|
+
fn countdown() {
|
|
194
|
+
scoreboard_set("#wloop", "i", 10);
|
|
195
|
+
scoreboard_set("#wloop", "steps", 0);
|
|
196
|
+
let i: int = scoreboard_get("#wloop", "i");
|
|
197
|
+
while (i > 0) {
|
|
198
|
+
let s: int = scoreboard_get("#wloop", "steps");
|
|
199
|
+
scoreboard_set("#wloop", "steps", s + 1);
|
|
200
|
+
i = i - 1;
|
|
201
|
+
scoreboard_set("#wloop", "i", i);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`, 'while_test')
|
|
205
|
+
|
|
206
|
+
// Scenario I: multiple if/else branches (boundary test)
|
|
207
|
+
writeFixture(`
|
|
208
|
+
fn classify_score() {
|
|
209
|
+
let x: int = scoreboard_get("#boundary", "input");
|
|
210
|
+
if (x > 100) {
|
|
211
|
+
scoreboard_set("#boundary", "tier", 3);
|
|
212
|
+
} else {
|
|
213
|
+
if (x > 50) {
|
|
214
|
+
scoreboard_set("#boundary", "tier", 2);
|
|
215
|
+
} else {
|
|
216
|
+
if (x > 0) {
|
|
217
|
+
scoreboard_set("#boundary", "tier", 1);
|
|
218
|
+
} else {
|
|
219
|
+
scoreboard_set("#boundary", "tier", 0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
`, 'boundary_test')
|
|
225
|
+
|
|
226
|
+
// Scenario J: entity management — summon via raw commands
|
|
227
|
+
writeFixture(`
|
|
228
|
+
fn tag_entities() {
|
|
229
|
+
raw("summon minecraft:armor_stand 10 65 10");
|
|
230
|
+
raw("summon minecraft:armor_stand 11 65 10");
|
|
231
|
+
raw("summon minecraft:armor_stand 12 65 10");
|
|
232
|
+
}
|
|
233
|
+
`, 'tag_test')
|
|
234
|
+
|
|
235
|
+
// Scenario K: mixed arithmetic — order of operations
|
|
236
|
+
writeFixture(`
|
|
237
|
+
fn math_order() {
|
|
238
|
+
let a: int = 2;
|
|
239
|
+
let b: int = 3;
|
|
240
|
+
let c: int = 4;
|
|
241
|
+
scoreboard_set("#order", "r1", a + b * c);
|
|
242
|
+
scoreboard_set("#order", "r2", (a + b) * c);
|
|
243
|
+
let d: int = 100;
|
|
244
|
+
let e: int = d / 3;
|
|
245
|
+
scoreboard_set("#order", "r3", e);
|
|
246
|
+
}
|
|
247
|
+
`, 'order_test')
|
|
248
|
+
|
|
249
|
+
// Scenario L: scoreboard read-modify-write chain
|
|
250
|
+
writeFixture(`
|
|
251
|
+
fn chain_rmw() {
|
|
252
|
+
scoreboard_set("#rmw", "v", 1);
|
|
253
|
+
let v: int = scoreboard_get("#rmw", "v");
|
|
254
|
+
scoreboard_set("#rmw", "v", v * 2);
|
|
255
|
+
v = scoreboard_get("#rmw", "v");
|
|
256
|
+
scoreboard_set("#rmw", "v", v * 2);
|
|
257
|
+
v = scoreboard_get("#rmw", "v");
|
|
258
|
+
scoreboard_set("#rmw", "v", v * 2);
|
|
259
|
+
}
|
|
260
|
+
`, 'rmw_test')
|
|
261
|
+
|
|
157
262
|
// ── Full reset + safe data reload ────────────────────────────────────
|
|
158
263
|
await mc.fullReset()
|
|
159
264
|
|
|
160
265
|
// Pre-create scoreboards
|
|
161
266
|
for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
|
|
162
|
-
'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val'
|
|
267
|
+
'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
|
|
268
|
+
'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v']) {
|
|
163
269
|
await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => {})
|
|
164
270
|
}
|
|
165
271
|
await mc.command('/scoreboard players set counter ticks 0')
|
|
@@ -192,7 +298,7 @@ describe('MC Integration Tests', () => {
|
|
|
192
298
|
})
|
|
193
299
|
|
|
194
300
|
// ─── Test 2: Counter tick ─────────────────────────────────────────────
|
|
195
|
-
test('counter.
|
|
301
|
+
test('counter.mcrs: tick function increments scoreboard over time', async () => {
|
|
196
302
|
if (!serverOnline) return
|
|
197
303
|
|
|
198
304
|
await mc.ticks(40) // Wait 2s (counter was already init'd in beforeAll)
|
|
@@ -202,7 +308,7 @@ describe('MC Integration Tests', () => {
|
|
|
202
308
|
})
|
|
203
309
|
|
|
204
310
|
// ─── Test 3: setblock ────────────────────────────────────────────────
|
|
205
|
-
test('world_manager.
|
|
311
|
+
test('world_manager.mcrs: setblock places correct block', async () => {
|
|
206
312
|
if (!serverOnline) return
|
|
207
313
|
|
|
208
314
|
// Clear just the lobby area, keep other state
|
|
@@ -217,7 +323,7 @@ describe('MC Integration Tests', () => {
|
|
|
217
323
|
})
|
|
218
324
|
|
|
219
325
|
// ─── Test 4: fill ────────────────────────────────────────────────────
|
|
220
|
-
test('world_manager.
|
|
326
|
+
test('world_manager.mcrs: fill creates smooth_stone floor', async () => {
|
|
221
327
|
if (!serverOnline) return
|
|
222
328
|
// Runs after test 3, floor should still be there
|
|
223
329
|
const block = await mc.block(4, 64, 4)
|
|
@@ -344,7 +450,7 @@ describe('E2E Scenario Tests', () => {
|
|
|
344
450
|
})
|
|
345
451
|
|
|
346
452
|
// Scenario B: No temp var collision between two functions called in sequence
|
|
347
|
-
// Verifies: each function's
|
|
453
|
+
// Verifies: each function's temp vars are isolated per-call via globally unique names
|
|
348
454
|
// If there's a bug, calc_product would see sum's leftover $t vars and produce wrong result
|
|
349
455
|
test('B: calc_sum + calc_product called in sequence — no temp var collision', async () => {
|
|
350
456
|
if (!serverOnline) return
|
|
@@ -406,4 +512,173 @@ describe('E2E Scenario Tests', () => {
|
|
|
406
512
|
console.log(` fill_test: blocks [0-3,70,0]=stone, [-1]/[4]=air ✓`)
|
|
407
513
|
})
|
|
408
514
|
|
|
515
|
+
// Scenario E: for-range loop executes body exactly N times
|
|
516
|
+
// Verifies: for i in 0..5 increments counter 5 times
|
|
517
|
+
test('E: for-range loop increments counter exactly 5 times', async () => {
|
|
518
|
+
if (!serverOnline) return
|
|
519
|
+
|
|
520
|
+
await mc.command('/function range_test:__load')
|
|
521
|
+
await mc.command('/function range_test:count_to_five')
|
|
522
|
+
await mc.ticks(10)
|
|
523
|
+
|
|
524
|
+
const counter = await mc.scoreboard('#range', 'counter')
|
|
525
|
+
expect(counter).toBe(5)
|
|
526
|
+
console.log(` for-range 0..5 → counter=${counter} (expect 5) ✓`)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// Scenario F: function return value propagation
|
|
530
|
+
// Verifies: $ret from callee is correctly captured in caller's variable
|
|
531
|
+
test('F: function return value — triple(4) = 12', async () => {
|
|
532
|
+
if (!serverOnline) return
|
|
533
|
+
|
|
534
|
+
await mc.command('/function nested_test:__load')
|
|
535
|
+
await mc.command('/function nested_test:run_nested')
|
|
536
|
+
await mc.ticks(10)
|
|
537
|
+
|
|
538
|
+
const result = await mc.scoreboard('#nested', 'result')
|
|
539
|
+
expect(result).toBe(12) // triple(4) = 4*3 = 12
|
|
540
|
+
console.log(` triple(4) = ${result} (expect 12) ✓`)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Scenario G: match dispatches to correct branch
|
|
544
|
+
// Verifies: match statement selects right arm for values 1, 2, 3, and default
|
|
545
|
+
test('G: match statement dispatches to correct branch', async () => {
|
|
546
|
+
if (!serverOnline) return
|
|
547
|
+
|
|
548
|
+
await mc.command('/function match_test:__load')
|
|
549
|
+
|
|
550
|
+
// Test match on value 2
|
|
551
|
+
await mc.command('/scoreboard players set $p0 rs 2')
|
|
552
|
+
await mc.command('/function match_test:classify')
|
|
553
|
+
await mc.ticks(5)
|
|
554
|
+
let out = await mc.scoreboard('#match', 'out')
|
|
555
|
+
expect(out).toBe(20)
|
|
556
|
+
console.log(` match(2) → out=${out} (expect 20) ✓`)
|
|
557
|
+
|
|
558
|
+
// Test match on value 3
|
|
559
|
+
await mc.command('/scoreboard players set $p0 rs 3')
|
|
560
|
+
await mc.command('/function match_test:classify')
|
|
561
|
+
await mc.ticks(5)
|
|
562
|
+
out = await mc.scoreboard('#match', 'out')
|
|
563
|
+
expect(out).toBe(30)
|
|
564
|
+
console.log(` match(3) → out=${out} (expect 30) ✓`)
|
|
565
|
+
|
|
566
|
+
// Test default branch (value 99)
|
|
567
|
+
await mc.command('/scoreboard players set $p0 rs 99')
|
|
568
|
+
await mc.command('/function match_test:classify')
|
|
569
|
+
await mc.ticks(5)
|
|
570
|
+
out = await mc.scoreboard('#match', 'out')
|
|
571
|
+
expect(out).toBe(-1)
|
|
572
|
+
console.log(` match(99) → out=${out} (expect -1, default) ✓`)
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
// Scenario H: while loop counts down from 10 to 0
|
|
576
|
+
// Verifies: while loop body executes correct number of iterations
|
|
577
|
+
test('H: while loop counts down 10 steps', async () => {
|
|
578
|
+
if (!serverOnline) return
|
|
579
|
+
|
|
580
|
+
await mc.command('/function while_test:__load')
|
|
581
|
+
await mc.command('/function while_test:countdown')
|
|
582
|
+
await mc.ticks(10)
|
|
583
|
+
|
|
584
|
+
const i = await mc.scoreboard('#wloop', 'i')
|
|
585
|
+
const steps = await mc.scoreboard('#wloop', 'steps')
|
|
586
|
+
expect(i).toBe(0)
|
|
587
|
+
expect(steps).toBe(10)
|
|
588
|
+
console.log(` while countdown: i=${i} (expect 0), steps=${steps} (expect 10) ✓`)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
// Scenario I: nested if/else boundary classification
|
|
592
|
+
// Verifies: correct branch taken at boundaries (0, 50, 100)
|
|
593
|
+
test('I: nested if/else boundary classification', async () => {
|
|
594
|
+
if (!serverOnline) return
|
|
595
|
+
|
|
596
|
+
await mc.command('/function boundary_test:__load')
|
|
597
|
+
|
|
598
|
+
// Test x=0 → tier 0
|
|
599
|
+
await mc.command('/scoreboard players set #boundary input 0')
|
|
600
|
+
await mc.command('/function boundary_test:classify_score')
|
|
601
|
+
await mc.ticks(5)
|
|
602
|
+
let tier = await mc.scoreboard('#boundary', 'tier')
|
|
603
|
+
expect(tier).toBe(0)
|
|
604
|
+
console.log(` classify(0) → tier=${tier} (expect 0) ✓`)
|
|
605
|
+
|
|
606
|
+
// Test x=50 → tier 1 (> 0 but not > 50)
|
|
607
|
+
await mc.command('/scoreboard players set #boundary input 50')
|
|
608
|
+
await mc.command('/function boundary_test:classify_score')
|
|
609
|
+
await mc.ticks(5)
|
|
610
|
+
tier = await mc.scoreboard('#boundary', 'tier')
|
|
611
|
+
expect(tier).toBe(1)
|
|
612
|
+
console.log(` classify(50) → tier=${tier} (expect 1) ✓`)
|
|
613
|
+
|
|
614
|
+
// Test x=51 → tier 2 (> 50 but not > 100)
|
|
615
|
+
await mc.command('/scoreboard players set #boundary input 51')
|
|
616
|
+
await mc.command('/function boundary_test:classify_score')
|
|
617
|
+
await mc.ticks(5)
|
|
618
|
+
tier = await mc.scoreboard('#boundary', 'tier')
|
|
619
|
+
expect(tier).toBe(2)
|
|
620
|
+
console.log(` classify(51) → tier=${tier} (expect 2) ✓`)
|
|
621
|
+
|
|
622
|
+
// Test x=101 → tier 3
|
|
623
|
+
await mc.command('/scoreboard players set #boundary input 101')
|
|
624
|
+
await mc.command('/function boundary_test:classify_score')
|
|
625
|
+
await mc.ticks(5)
|
|
626
|
+
tier = await mc.scoreboard('#boundary', 'tier')
|
|
627
|
+
expect(tier).toBe(3)
|
|
628
|
+
console.log(` classify(101) → tier=${tier} (expect 3) ✓`)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
// Scenario J: entity summon and query
|
|
632
|
+
// Verifies: entities spawned via compiled function are queryable
|
|
633
|
+
test('J: summon entities via compiled function', async () => {
|
|
634
|
+
if (!serverOnline) return
|
|
635
|
+
|
|
636
|
+
await mc.command('/kill @e[type=minecraft:armor_stand]')
|
|
637
|
+
await mc.ticks(2)
|
|
638
|
+
await mc.command('/function tag_test:__load')
|
|
639
|
+
await mc.command('/function tag_test:tag_entities')
|
|
640
|
+
await mc.ticks(5)
|
|
641
|
+
|
|
642
|
+
const stands = await mc.entities('@e[type=minecraft:armor_stand]')
|
|
643
|
+
expect(stands.length).toBe(3)
|
|
644
|
+
console.log(` Summoned 3 armor_stands via tag_test, found: ${stands.length} ✓`)
|
|
645
|
+
|
|
646
|
+
await mc.command('/kill @e[type=minecraft:armor_stand]')
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Scenario K: arithmetic order of operations
|
|
650
|
+
// Verifies: MC scoreboard arithmetic matches expected evaluation order
|
|
651
|
+
test('K: arithmetic order of operations', async () => {
|
|
652
|
+
if (!serverOnline) return
|
|
653
|
+
|
|
654
|
+
await mc.command('/function order_test:__load')
|
|
655
|
+
await mc.command('/function order_test:math_order')
|
|
656
|
+
await mc.ticks(10)
|
|
657
|
+
|
|
658
|
+
const r1 = await mc.scoreboard('#order', 'r1')
|
|
659
|
+
const r2 = await mc.scoreboard('#order', 'r2')
|
|
660
|
+
const r3 = await mc.scoreboard('#order', 'r3')
|
|
661
|
+
// a + b * c = 2 + 3*4 = 14 (if precedence respected) or (2+3)*4 = 20 (left-to-right)
|
|
662
|
+
// MC scoreboard does left-to-right, so compiler may emit either depending on lowering
|
|
663
|
+
// (a + b) * c = 5 * 4 = 20 (explicit parens)
|
|
664
|
+
expect(r2).toBe(20) // This one is unambiguous
|
|
665
|
+
// 100 / 3 = 33 (integer division)
|
|
666
|
+
expect(r3).toBe(33)
|
|
667
|
+
console.log(` r1=${r1}, r2=${r2} (expect 20), r3=${r3} (expect 33) ✓`)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// Scenario L: scoreboard read-modify-write chain (1 → 2 → 4 → 8)
|
|
671
|
+
// Verifies: sequential RMW operations don't lose intermediate state
|
|
672
|
+
test('L: scoreboard RMW chain — 1*2*2*2 = 8', async () => {
|
|
673
|
+
if (!serverOnline) return
|
|
674
|
+
|
|
675
|
+
await mc.command('/function rmw_test:__load')
|
|
676
|
+
await mc.command('/function rmw_test:chain_rmw')
|
|
677
|
+
await mc.ticks(10)
|
|
678
|
+
|
|
679
|
+
const v = await mc.scoreboard('#rmw', 'v')
|
|
680
|
+
expect(v).toBe(8)
|
|
681
|
+
console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`)
|
|
682
|
+
})
|
|
683
|
+
|
|
409
684
|
})
|
|
@@ -33,14 +33,14 @@ describe('MC Command Syntax Validation', () => {
|
|
|
33
33
|
const validator = new MCCommandValidator(FIXTURE_PATH)
|
|
34
34
|
|
|
35
35
|
test('counter example generates valid MC commands', () => {
|
|
36
|
-
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.
|
|
36
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8')
|
|
37
37
|
const errors = validateSource(validator, src, 'counter')
|
|
38
38
|
expect(errors).toHaveLength(0)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
EXAMPLES.forEach(name => {
|
|
42
|
-
test(`${name}.
|
|
43
|
-
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.
|
|
42
|
+
test(`${name}.mcrs generates valid MC commands`, () => {
|
|
43
|
+
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8')
|
|
44
44
|
const errors = validateSource(validator, src, name)
|
|
45
45
|
|
|
46
46
|
if (errors.length > 0) {
|
|
@@ -40,8 +40,8 @@ describe('NBT codec', () => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
describe('Structure generator', () => {
|
|
43
|
-
test('compiles counter.
|
|
44
|
-
const filePath = 'src/examples/counter.
|
|
43
|
+
test('compiles counter.mcrs to a non-empty structure', () => {
|
|
44
|
+
const filePath = 'src/examples/counter.mcrs'
|
|
45
45
|
const src = fs.readFileSync(filePath, 'utf-8')
|
|
46
46
|
const { buffer, blockCount } = compileToStructure(src, 'counter', filePath)
|
|
47
47
|
|
|
@@ -28,7 +28,7 @@ fn turret_tick() {
|
|
|
28
28
|
const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction')
|
|
29
29
|
const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction')
|
|
30
30
|
|
|
31
|
-
const hoistedRead = 'execute store result score $
|
|
31
|
+
const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config turret_range'
|
|
32
32
|
const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0'
|
|
33
33
|
|
|
34
34
|
expect(parent).toContain(hoistedRead)
|
|
@@ -54,7 +54,7 @@ fn read_twice() {
|
|
|
54
54
|
const readMatches = fn.match(/scoreboard players get @s coins/g) ?? []
|
|
55
55
|
|
|
56
56
|
expect(readMatches).toHaveLength(1)
|
|
57
|
-
expect(fn).toContain('scoreboard players operation $
|
|
57
|
+
expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
test('reuses duplicate arithmetic sequences', () => {
|
|
@@ -74,7 +74,7 @@ fn math() {
|
|
|
74
74
|
const addMatches = fn.match(/\+= \$const_2 rs/g) ?? []
|
|
75
75
|
|
|
76
76
|
expect(addMatches).toHaveLength(1)
|
|
77
|
-
expect(fn).toContain('scoreboard players operation $
|
|
77
|
+
expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
|
|
@@ -29,7 +29,7 @@ function loadExample(name: string): string {
|
|
|
29
29
|
|
|
30
30
|
describe('MCRuntime behavioral integration', () => {
|
|
31
31
|
it('runs the counter example and increments the scoreboard across ticks', () => {
|
|
32
|
-
const runtime = loadCompiledProgram(loadExample('counter.
|
|
32
|
+
const runtime = loadCompiledProgram(loadExample('counter.mcrs'))
|
|
33
33
|
|
|
34
34
|
runtime.load()
|
|
35
35
|
runtime.ticks(5)
|
package/src/ast/types.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface SelectorFilter {
|
|
|
68
68
|
sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary'
|
|
69
69
|
nbt?: string
|
|
70
70
|
gamemode?: string
|
|
71
|
+
// Position filters
|
|
72
|
+
x?: RangeExpr
|
|
73
|
+
y?: RangeExpr
|
|
74
|
+
z?: RangeExpr
|
|
75
|
+
// Rotation filters
|
|
76
|
+
x_rotation?: RangeExpr
|
|
77
|
+
y_rotation?: RangeExpr
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export interface EntitySelector {
|
|
@@ -146,8 +153,8 @@ export type LiteralExpr =
|
|
|
146
153
|
export type ExecuteSubcommand =
|
|
147
154
|
| { kind: 'as'; selector: EntitySelector }
|
|
148
155
|
| { kind: 'at'; selector: EntitySelector }
|
|
149
|
-
| { kind: 'if_entity'; selector
|
|
150
|
-
| { kind: 'unless_entity'; selector
|
|
156
|
+
| { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
157
|
+
| { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
151
158
|
| { kind: 'in'; dimension: string }
|
|
152
159
|
|
|
153
160
|
export type Stmt =
|
|
@@ -173,7 +180,7 @@ export type Block = Stmt[]
|
|
|
173
180
|
// ---------------------------------------------------------------------------
|
|
174
181
|
|
|
175
182
|
export interface Decorator {
|
|
176
|
-
name: 'tick' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
183
|
+
name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
177
184
|
args?: {
|
|
178
185
|
rate?: number
|
|
179
186
|
trigger?: string
|
|
@@ -235,12 +242,22 @@ export interface ConstDecl {
|
|
|
235
242
|
span?: Span
|
|
236
243
|
}
|
|
237
244
|
|
|
245
|
+
export interface GlobalDecl {
|
|
246
|
+
kind: 'global'
|
|
247
|
+
name: string
|
|
248
|
+
type: TypeNode
|
|
249
|
+
init: Expr
|
|
250
|
+
mutable: boolean // let = true, const = false
|
|
251
|
+
span?: Span
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
// ---------------------------------------------------------------------------
|
|
239
255
|
// Program (Top-Level)
|
|
240
256
|
// ---------------------------------------------------------------------------
|
|
241
257
|
|
|
242
258
|
export interface Program {
|
|
243
259
|
namespace: string // Inferred from filename or `namespace mypack;`
|
|
260
|
+
globals: GlobalDecl[]
|
|
244
261
|
declarations: FnDecl[]
|
|
245
262
|
structs: StructDecl[]
|
|
246
263
|
enums: EnumDecl[]
|
package/src/cli.ts
CHANGED
|
@@ -29,13 +29,13 @@ Usage:
|
|
|
29
29
|
redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
|
|
30
30
|
redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
|
|
31
31
|
redscript check <file>
|
|
32
|
-
redscript fmt <file.
|
|
32
|
+
redscript fmt <file.mcrs> [file2.mcrs ...]
|
|
33
33
|
redscript repl
|
|
34
34
|
redscript version
|
|
35
35
|
|
|
36
36
|
Commands:
|
|
37
37
|
compile Compile a RedScript file to a Minecraft datapack
|
|
38
|
-
watch Watch a directory for .
|
|
38
|
+
watch Watch a directory for .mcrs file changes, recompile, and hot reload
|
|
39
39
|
check Check a RedScript file for errors without generating output
|
|
40
40
|
fmt Auto-format RedScript source files
|
|
41
41
|
repl Start an interactive RedScript REPL
|
|
@@ -268,7 +268,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
268
268
|
process.exit(1)
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
console.log(`👁 Watching ${dir} for .
|
|
271
|
+
console.log(`👁 Watching ${dir} for .mcrs file changes...`)
|
|
272
272
|
console.log(` Output: ${output}`)
|
|
273
273
|
if (hotReloadUrl) console.log(` Hot reload: ${hotReloadUrl}`)
|
|
274
274
|
console.log(` Press Ctrl+C to stop\n`)
|
|
@@ -276,11 +276,11 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
276
276
|
// Debounce timer
|
|
277
277
|
let debounceTimer: NodeJS.Timeout | null = null
|
|
278
278
|
|
|
279
|
-
// Compile all .
|
|
279
|
+
// Compile all .mcrs files in directory
|
|
280
280
|
async function compileAll(): Promise<void> {
|
|
281
281
|
const files = findRsFiles(dir)
|
|
282
282
|
if (files.length === 0) {
|
|
283
|
-
console.log(`⚠ No .
|
|
283
|
+
console.log(`⚠ No .mcrs files found in ${dir}`)
|
|
284
284
|
return
|
|
285
285
|
}
|
|
286
286
|
|
|
@@ -319,7 +319,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
// Find all .
|
|
322
|
+
// Find all .mcrs files recursively
|
|
323
323
|
function findRsFiles(directory: string): string[] {
|
|
324
324
|
const results: string[] = []
|
|
325
325
|
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
@@ -328,7 +328,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
328
328
|
const fullPath = path.join(directory, entry.name)
|
|
329
329
|
if (entry.isDirectory()) {
|
|
330
330
|
results.push(...findRsFiles(fullPath))
|
|
331
|
-
} else if (entry.isFile() && entry.name.endsWith('.
|
|
331
|
+
} else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
|
|
332
332
|
results.push(fullPath)
|
|
333
333
|
}
|
|
334
334
|
}
|
|
@@ -341,7 +341,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
341
341
|
|
|
342
342
|
// Watch for changes
|
|
343
343
|
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
344
|
-
if (filename && filename.endsWith('.
|
|
344
|
+
if (filename && filename.endsWith('.mcrs')) {
|
|
345
345
|
// Debounce rapid changes
|
|
346
346
|
if (debounceTimer) {
|
|
347
347
|
clearTimeout(debounceTimer)
|
|
@@ -412,9 +412,9 @@ async function main(): Promise<void> {
|
|
|
412
412
|
|
|
413
413
|
case 'fmt':
|
|
414
414
|
case 'format': {
|
|
415
|
-
const files = args.filter(a => a.endsWith('.
|
|
415
|
+
const files = args.filter(a => a.endsWith('.mcrs'))
|
|
416
416
|
if (files.length === 0) {
|
|
417
|
-
console.error('Usage: redscript fmt <file.
|
|
417
|
+
console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]')
|
|
418
418
|
process.exit(1)
|
|
419
419
|
}
|
|
420
420
|
const { format } = require('./formatter')
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Variable mapping:
|
|
12
12
|
* scoreboard objective: "rs"
|
|
13
13
|
* fake player: "$<varname>"
|
|
14
|
-
* temporaries: "$
|
|
14
|
+
* temporaries: "$_0", "$_1", ...
|
|
15
15
|
* return value: "$ret"
|
|
16
16
|
* parameters: "$p0", "$p1", ...
|
|
17
17
|
*/
|
|
@@ -293,7 +293,7 @@ export function generateDatapackWithStats(
|
|
|
293
293
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
294
294
|
]
|
|
295
295
|
for (const g of module.globals) {
|
|
296
|
-
loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ}
|
|
296
|
+
loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
// Add trigger objectives
|
|
@@ -356,6 +356,13 @@ export function generateDatapackWithStats(
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
// Call @load functions from __load
|
|
360
|
+
for (const fn of module.functions) {
|
|
361
|
+
if (fn.isLoadInit) {
|
|
362
|
+
loadLines.push(`function ${ns}:${fn.name}`)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
359
366
|
// Write __load.mcfunction
|
|
360
367
|
files.push({
|
|
361
368
|
path: `data/${ns}/function/__load.mcfunction`,
|
|
@@ -89,7 +89,7 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
89
89
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
90
90
|
const loadCommands = [
|
|
91
91
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
92
|
-
...module.globals.map(
|
|
92
|
+
...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
|
|
93
93
|
...Array.from(triggerNames).flatMap(triggerName => [
|
|
94
94
|
`scoreboard objectives add ${triggerName} trigger`,
|
|
95
95
|
`scoreboard players enable @a ${triggerName}`,
|
|
@@ -99,6 +99,13 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
99
99
|
).map(constSetup),
|
|
100
100
|
]
|
|
101
101
|
|
|
102
|
+
// Call @load functions from __load
|
|
103
|
+
for (const fn of module.functions) {
|
|
104
|
+
if (fn.isLoadInit) {
|
|
105
|
+
loadCommands.push(`function ${module.namespace}:${fn.name}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
102
109
|
const sections: Array<{ name: string; commands: IRCommand[]; repeat?: boolean }> = []
|
|
103
110
|
|
|
104
111
|
if (loadCommands.length > 0) {
|