redscript-mc 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- 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 +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- 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 +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- 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/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- 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/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /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/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
|
@@ -124,6 +124,184 @@ fn main() {
|
|
|
124
124
|
expect(mainFn).toContain('"objective":"rs"');
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
describe('timer builtins', () => {
|
|
128
|
+
it('generates scheduled timer helper functions', () => {
|
|
129
|
+
const files = compile(`
|
|
130
|
+
fn main() {
|
|
131
|
+
let intervalId: int = setInterval(20, () => {
|
|
132
|
+
say("tick");
|
|
133
|
+
});
|
|
134
|
+
setTimeout(100, () => {
|
|
135
|
+
say("later");
|
|
136
|
+
});
|
|
137
|
+
clearInterval(intervalId);
|
|
138
|
+
}
|
|
139
|
+
`);
|
|
140
|
+
const mainFn = getFunction(files, 'main');
|
|
141
|
+
const intervalFn = getFunction(files, '__interval_0');
|
|
142
|
+
const intervalBodyFn = getFunction(files, '__interval_body_0');
|
|
143
|
+
const timeoutFn = getFunction(files, '__timeout_0');
|
|
144
|
+
expect(mainFn).toBeDefined();
|
|
145
|
+
expect(mainFn).toContain('schedule function test:__interval_0 20t');
|
|
146
|
+
expect(mainFn).toContain('schedule function test:__timeout_0 100t');
|
|
147
|
+
expect(mainFn).toContain('schedule clear test:__interval_0');
|
|
148
|
+
expect(intervalFn).toContain('function test:__interval_body_0');
|
|
149
|
+
expect(intervalFn).toContain('schedule function test:__interval_0 20t');
|
|
150
|
+
expect(intervalBodyFn).toContain('say tick');
|
|
151
|
+
expect(timeoutFn).toContain('say later');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('is type narrowing', () => {
|
|
155
|
+
it('type checks and compiles entity narrowing inside foreach blocks', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
fn main() {
|
|
158
|
+
foreach (e in @e) {
|
|
159
|
+
if (e is Player) {
|
|
160
|
+
kill(e);
|
|
161
|
+
}
|
|
162
|
+
if (e is Zombie) {
|
|
163
|
+
kill(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
expect(typeCheck(source)).toEqual([]);
|
|
169
|
+
const files = compile(source);
|
|
170
|
+
const mainFn = getFunction(files, 'main');
|
|
171
|
+
const foreachFn = getSubFunction(files, 'main', 'foreach_0');
|
|
172
|
+
const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'));
|
|
173
|
+
expect(mainFn).toContain('execute as @e run function test:main/foreach_0');
|
|
174
|
+
expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_');
|
|
175
|
+
expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_');
|
|
176
|
+
expect(thenFiles).toHaveLength(2);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('impl blocks', () => {
|
|
180
|
+
it('compiles static and instance impl methods end to end', () => {
|
|
181
|
+
const source = `
|
|
182
|
+
struct Point { x: int, y: int }
|
|
183
|
+
|
|
184
|
+
impl Point {
|
|
185
|
+
fn new(x: int, y: int) -> Point {
|
|
186
|
+
return { x: x, y: y };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn distance(self) -> int {
|
|
190
|
+
return self.x + self.y;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn main() {
|
|
195
|
+
let p: Point = Point::new(1, 2);
|
|
196
|
+
let d: int = p.distance();
|
|
197
|
+
say("\${d}");
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
expect(typeCheck(source)).toEqual([]);
|
|
201
|
+
const files = compile(source);
|
|
202
|
+
const mainFn = getFunction(files, 'main');
|
|
203
|
+
const staticFn = getFunction(files, 'Point_new');
|
|
204
|
+
const instanceFn = getFunction(files, 'Point_distance');
|
|
205
|
+
expect(mainFn).toContain('function test:Point_new');
|
|
206
|
+
expect(mainFn).toContain('function test:Point_distance');
|
|
207
|
+
expect(staticFn).toBeDefined();
|
|
208
|
+
expect(instanceFn).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('namespace prefixing', () => {
|
|
212
|
+
it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
|
|
213
|
+
const files = compile(`
|
|
214
|
+
fn main() {
|
|
215
|
+
scoreboard_set("timer", #rs, 100);
|
|
216
|
+
scoreboard_set(@s, "timer", 100);
|
|
217
|
+
scoreboard_set(@s, #health, 20);
|
|
218
|
+
scoreboard_set(@s, "custom.timer", 1);
|
|
219
|
+
}
|
|
220
|
+
`, 'pack');
|
|
221
|
+
const mainFn = getFunction(files, 'main');
|
|
222
|
+
expect(mainFn).toContain('scoreboard players set timer rs 100');
|
|
223
|
+
expect(mainFn).toContain('scoreboard players set @s pack.timer 100');
|
|
224
|
+
expect(mainFn).toContain('scoreboard players set @s health 20');
|
|
225
|
+
expect(mainFn).toContain('scoreboard players set @s custom.timer 1');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('Timer OOP API', () => {
|
|
229
|
+
it('compiles the Timer impl API end to end', () => {
|
|
230
|
+
const source = `
|
|
231
|
+
struct Timer {
|
|
232
|
+
_id: int,
|
|
233
|
+
_duration: int
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
impl Timer {
|
|
237
|
+
fn new(duration: int) -> Timer {
|
|
238
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
239
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
240
|
+
return { _id: 0, _duration: duration };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn start(self) {
|
|
244
|
+
scoreboard_set("timer_active", #rs, 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn pause(self) {
|
|
248
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn reset(self) {
|
|
252
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn done(self) -> bool {
|
|
256
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
257
|
+
return ticks >= self._duration;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn tick(self) {
|
|
261
|
+
let active: int = scoreboard_get("timer_active", #rs);
|
|
262
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
263
|
+
|
|
264
|
+
if (active == 1) {
|
|
265
|
+
if (ticks < self._duration) {
|
|
266
|
+
scoreboard_set("timer_ticks", #rs, ticks + 1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn main() {
|
|
273
|
+
let t: Timer = Timer::new(100);
|
|
274
|
+
t.start();
|
|
275
|
+
t.tick();
|
|
276
|
+
let finished: bool = t.done();
|
|
277
|
+
if (finished) {
|
|
278
|
+
say("done");
|
|
279
|
+
}
|
|
280
|
+
t.pause();
|
|
281
|
+
t.reset();
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
expect(typeCheck(source)).toEqual([]);
|
|
285
|
+
const files = compile(source, 'timerapi');
|
|
286
|
+
const mainFn = getFunction(files, 'main');
|
|
287
|
+
const newFn = getFunction(files, 'Timer_new');
|
|
288
|
+
const startFn = getFunction(files, 'Timer_start');
|
|
289
|
+
const tickFn = getFunction(files, 'Timer_tick');
|
|
290
|
+
const doneFn = getFunction(files, 'Timer_done');
|
|
291
|
+
const pauseFn = getFunction(files, 'Timer_pause');
|
|
292
|
+
const resetFn = getFunction(files, 'Timer_reset');
|
|
293
|
+
expect(mainFn).toContain('function timerapi:Timer_new');
|
|
294
|
+
expect(mainFn).toContain('function timerapi:Timer_start');
|
|
295
|
+
expect(mainFn).toContain('function timerapi:Timer_tick');
|
|
296
|
+
expect(mainFn).toContain('function timerapi:Timer_done');
|
|
297
|
+
expect(newFn).toContain('scoreboard players set timer_ticks rs 0');
|
|
298
|
+
expect(startFn).toContain('scoreboard players set timer_active rs 1');
|
|
299
|
+
expect(tickFn).toContain('scoreboard players get timer_active rs');
|
|
300
|
+
expect(doneFn).toContain('scoreboard players get timer_ticks rs');
|
|
301
|
+
expect(pauseFn).toContain('scoreboard players set timer_active rs 0');
|
|
302
|
+
expect(resetFn).toContain('scoreboard players set timer_ticks rs 0');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
127
305
|
describe('advancement event decorators', () => {
|
|
128
306
|
it('generates advancement json with reward function path', () => {
|
|
129
307
|
const source = `
|
|
@@ -276,12 +454,12 @@ fn test() {
|
|
|
276
454
|
}
|
|
277
455
|
`;
|
|
278
456
|
const fn = getFunction(compile(source), 'test');
|
|
279
|
-
expect(fn).toContain('scoreboard objectives setdisplay sidebar kills');
|
|
280
|
-
expect(fn).toContain('scoreboard objectives setdisplay list coins');
|
|
281
|
-
expect(fn).toContain('scoreboard objectives setdisplay belowName hp');
|
|
457
|
+
expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills');
|
|
458
|
+
expect(fn).toContain('scoreboard objectives setdisplay list test.coins');
|
|
459
|
+
expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp');
|
|
282
460
|
expect(fn).toContain('scoreboard objectives setdisplay sidebar');
|
|
283
|
-
expect(fn).toContain('scoreboard objectives add kills playerKillCount "Kill Count"');
|
|
284
|
-
expect(fn).toContain('scoreboard objectives remove kills');
|
|
461
|
+
expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
|
|
462
|
+
expect(fn).toContain('scoreboard objectives remove test.kills');
|
|
285
463
|
});
|
|
286
464
|
it('compiles bossbar builtins', () => {
|
|
287
465
|
const source = `
|
|
@@ -306,7 +484,7 @@ fn test() {
|
|
|
306
484
|
expect(fn).toContain('bossbar set ns:health visible true');
|
|
307
485
|
expect(fn).toContain('bossbar set ns:health players @a');
|
|
308
486
|
expect(fn).toContain('bossbar remove ns:health');
|
|
309
|
-
expect(fn).toMatch(/execute store result score \$
|
|
487
|
+
expect(fn).toMatch(/execute store result score \$_\d+ rs run bossbar get ns:health value/);
|
|
310
488
|
});
|
|
311
489
|
it('compiles team builtins', () => {
|
|
312
490
|
const source = `
|
|
@@ -485,7 +663,7 @@ fn test() -> int {
|
|
|
485
663
|
const fn = getFunction(files, 'test');
|
|
486
664
|
expect(fn).toBeDefined();
|
|
487
665
|
expect(fn).toContain('execute store result score');
|
|
488
|
-
expect(fn).toContain('scoreboard players get PlayerName kill_count');
|
|
666
|
+
expect(fn).toContain('scoreboard players get PlayerName test.kill_count');
|
|
489
667
|
});
|
|
490
668
|
it('compiles scoreboard_get with @s selector', () => {
|
|
491
669
|
const source = `
|
|
@@ -497,7 +675,7 @@ fn test() -> int {
|
|
|
497
675
|
const files = compile(source);
|
|
498
676
|
const fn = getFunction(files, 'test');
|
|
499
677
|
expect(fn).toBeDefined();
|
|
500
|
-
expect(fn).toContain('scoreboard players get @s kill_count');
|
|
678
|
+
expect(fn).toContain('scoreboard players get @s test.kill_count');
|
|
501
679
|
});
|
|
502
680
|
it('compiles scoreboard_set with constant value', () => {
|
|
503
681
|
const source = `
|
|
@@ -508,7 +686,7 @@ fn test() {
|
|
|
508
686
|
const files = compile(source);
|
|
509
687
|
const fn = getFunction(files, 'test');
|
|
510
688
|
expect(fn).toBeDefined();
|
|
511
|
-
expect(fn).toContain('scoreboard players set PlayerName kill_count 100');
|
|
689
|
+
expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100');
|
|
512
690
|
});
|
|
513
691
|
it('compiles scoreboard_set with variable value', () => {
|
|
514
692
|
const source = `
|
|
@@ -522,7 +700,7 @@ fn test() {
|
|
|
522
700
|
.filter(f => f.path.includes('test'))
|
|
523
701
|
.map(f => f.content)
|
|
524
702
|
.join('\n');
|
|
525
|
-
expect(allContent).toContain('execute store result score @s score');
|
|
703
|
+
expect(allContent).toContain('execute store result score @s test.score');
|
|
526
704
|
});
|
|
527
705
|
it('compiles score() as expression', () => {
|
|
528
706
|
const source = `
|
|
@@ -548,7 +726,7 @@ fn double_score() -> int {
|
|
|
548
726
|
const files = compile(source);
|
|
549
727
|
const fn = getFunction(files, 'double_score');
|
|
550
728
|
expect(fn).toBeDefined();
|
|
551
|
-
expect(fn).toContain('scoreboard players get @s points');
|
|
729
|
+
expect(fn).toContain('scoreboard players get @s test.points');
|
|
552
730
|
});
|
|
553
731
|
});
|
|
554
732
|
describe('Built-in functions', () => {
|
|
@@ -586,19 +764,19 @@ fn double_score() -> int {
|
|
|
586
764
|
const source = 'fn test() { let x: int = random(1, 10); }';
|
|
587
765
|
const files = compile(source);
|
|
588
766
|
const fn = getFunction(files, 'test');
|
|
589
|
-
expect(fn).toContain('scoreboard players random $
|
|
767
|
+
expect(fn).toContain('scoreboard players random $_0 rs 1 10');
|
|
590
768
|
});
|
|
591
769
|
it('compiles random_native()', () => {
|
|
592
770
|
const source = 'fn test() { let x: int = random_native(1, 6); }';
|
|
593
771
|
const files = compile(source);
|
|
594
772
|
const fn = getFunction(files, 'test');
|
|
595
|
-
expect(fn).toContain('execute store result score $
|
|
773
|
+
expect(fn).toContain('execute store result score $_0 rs run random value 1 6');
|
|
596
774
|
});
|
|
597
775
|
it('compiles random_native() with zero min', () => {
|
|
598
776
|
const source = 'fn test() { let x: int = random_native(0, 100); }';
|
|
599
777
|
const files = compile(source);
|
|
600
778
|
const fn = getFunction(files, 'test');
|
|
601
|
-
expect(fn).toContain('execute store result score $
|
|
779
|
+
expect(fn).toContain('execute store result score $_0 rs run random value 0 100');
|
|
602
780
|
});
|
|
603
781
|
it('compiles random_sequence()', () => {
|
|
604
782
|
const source = 'fn test() { random_sequence("loot"); random_sequence("loot", 9); }';
|
|
@@ -1079,7 +1257,7 @@ fn handle_claim() {
|
|
|
1079
1257
|
expect(fn).toContain('if entity @s[tag=boss]');
|
|
1080
1258
|
});
|
|
1081
1259
|
});
|
|
1082
|
-
describe('Real program: zombie_game.
|
|
1260
|
+
describe('Real program: zombie_game.mcrs', () => {
|
|
1083
1261
|
const source = `
|
|
1084
1262
|
// A zombie survival game logic
|
|
1085
1263
|
// Kills nearby zombies and tracks score
|
|
@@ -1342,10 +1520,10 @@ fn heal(amount: int) {
|
|
|
1342
1520
|
});
|
|
1343
1521
|
describe('backward compat: string objective still works', () => {
|
|
1344
1522
|
const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`;
|
|
1345
|
-
it('
|
|
1523
|
+
it('prefixes plain string objectives with the active namespace', () => {
|
|
1346
1524
|
const files = compile(source, 'compat');
|
|
1347
1525
|
const fn = getFunction(files, 'test');
|
|
1348
|
-
expect(fn).toContain('scoreboard players get @s kills');
|
|
1526
|
+
expect(fn).toContain('scoreboard players get @s compat.kills');
|
|
1349
1527
|
});
|
|
1350
1528
|
});
|
|
1351
1529
|
describe('#mc_name with fake player target', () => {
|
|
@@ -1525,4 +1703,145 @@ describe('for-range loop', () => {
|
|
|
1525
1703
|
expect(subFn?.content).toContain('execute if score $x rs matches ..7 run function forloop2:test/__for_0');
|
|
1526
1704
|
});
|
|
1527
1705
|
});
|
|
1706
|
+
// ---------------------------------------------------------------------------
|
|
1707
|
+
// NBT Structured Parameters
|
|
1708
|
+
// ---------------------------------------------------------------------------
|
|
1709
|
+
describe('NBT parameters', () => {
|
|
1710
|
+
it('compiles give with NBT struct', () => {
|
|
1711
|
+
const src = `fn test() { give(@s, "minecraft:diamond_sword", 1, { display: { Name: "Excalibur" } }); }`;
|
|
1712
|
+
const files = compile(src, 'nbtparam');
|
|
1713
|
+
const fn = getFunction(files, 'test');
|
|
1714
|
+
expect(fn).toContain('give @s minecraft:diamond_sword{display:{Name:"Excalibur"}} 1');
|
|
1715
|
+
});
|
|
1716
|
+
it('compiles give with nested NBT and arrays', () => {
|
|
1717
|
+
const src = `fn test() { give(@s, "minecraft:stick", 1, { display: { Name: "Magic Wand" }, Enchantments: [{ id: "sharpness", lvl: 5 }] }); }`;
|
|
1718
|
+
const files = compile(src, 'nbtparam2');
|
|
1719
|
+
const fn = getFunction(files, 'test');
|
|
1720
|
+
expect(fn).toContain('{display:{Name:"Magic Wand"},Enchantments:[{id:"sharpness",lvl:5}]}');
|
|
1721
|
+
});
|
|
1722
|
+
it('compiles summon with NBT', () => {
|
|
1723
|
+
const src = `fn test() { summon("minecraft:zombie", 0, 64, 0, { CustomName: "Boss", NoAI: true }); }`;
|
|
1724
|
+
const files = compile(src, 'nbtsummon');
|
|
1725
|
+
const fn = getFunction(files, 'test');
|
|
1726
|
+
expect(fn).toContain('summon minecraft:zombie 0 64 0 {CustomName:"Boss",NoAI:1b}');
|
|
1727
|
+
});
|
|
1728
|
+
it('compiles give with bool values in NBT', () => {
|
|
1729
|
+
const src = `fn test() { give(@s, "minecraft:shield", 1, { Unbreakable: true }); }`;
|
|
1730
|
+
const files = compile(src, 'nbtbool');
|
|
1731
|
+
const fn = getFunction(files, 'test');
|
|
1732
|
+
expect(fn).toContain('{Unbreakable:1b}');
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
// ---------------------------------------------------------------------------
|
|
1736
|
+
// Set Operations
|
|
1737
|
+
// ---------------------------------------------------------------------------
|
|
1738
|
+
describe('Set operations', () => {
|
|
1739
|
+
it('creates a new set', () => {
|
|
1740
|
+
const src = `fn test() { let s = set_new(); }`;
|
|
1741
|
+
const files = compile(src, 'settest');
|
|
1742
|
+
const fn = getFunction(files, 'test');
|
|
1743
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
|
|
1744
|
+
});
|
|
1745
|
+
it('adds to a set with uniqueness check', () => {
|
|
1746
|
+
const src = `fn test() { let s = set_new(); set_add(s, "apple"); }`;
|
|
1747
|
+
const files = compile(src, 'setadd');
|
|
1748
|
+
const fn = getFunction(files, 'test');
|
|
1749
|
+
expect(fn).toContain('execute unless data storage rs:sets __set_0[{value:apple}] run data modify storage rs:sets __set_0 append value {value:apple}');
|
|
1750
|
+
});
|
|
1751
|
+
it('checks set membership', () => {
|
|
1752
|
+
const src = `fn test() { let s = set_new(); set_add(s, "x"); let has = set_contains(s, "x"); }`;
|
|
1753
|
+
const files = compile(src, 'setcontains');
|
|
1754
|
+
const fn = getFunction(files, 'test');
|
|
1755
|
+
expect(fn).toContain('if data storage rs:sets __set_0[{value:x}]');
|
|
1756
|
+
});
|
|
1757
|
+
it('removes from a set', () => {
|
|
1758
|
+
const src = `fn test() { let s = set_new(); set_add(s, "y"); set_remove(s, "y"); }`;
|
|
1759
|
+
const files = compile(src, 'setremove');
|
|
1760
|
+
const fn = getFunction(files, 'test');
|
|
1761
|
+
expect(fn).toContain('data remove storage rs:sets __set_0[{value:y}]');
|
|
1762
|
+
});
|
|
1763
|
+
it('clears a set', () => {
|
|
1764
|
+
const src = `fn test() { let s = set_new(); set_clear(s); }`;
|
|
1765
|
+
const files = compile(src, 'setclear');
|
|
1766
|
+
const fn = getFunction(files, 'test');
|
|
1767
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
// ---------------------------------------------------------------------------
|
|
1771
|
+
// Method Syntax Sugar
|
|
1772
|
+
// ---------------------------------------------------------------------------
|
|
1773
|
+
describe('Method syntax sugar', () => {
|
|
1774
|
+
it('transforms obj.method() to method(obj)', () => {
|
|
1775
|
+
const src = `fn test() { let s = set_new(); s.clear(); }`;
|
|
1776
|
+
const files = compile(src, 'method1');
|
|
1777
|
+
const fn = getFunction(files, 'test');
|
|
1778
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
|
|
1779
|
+
});
|
|
1780
|
+
it('transforms obj.method(arg) to method(obj, arg)', () => {
|
|
1781
|
+
const src = `fn test() { let s = set_new(); s.add("apple"); }`;
|
|
1782
|
+
const files = compile(src, 'method2');
|
|
1783
|
+
const fn = getFunction(files, 'test');
|
|
1784
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 append value {value:apple}');
|
|
1785
|
+
});
|
|
1786
|
+
it('transforms obj.method(arg) with contains', () => {
|
|
1787
|
+
const src = `fn test() { let s = set_new(); s.add("x"); let r = s.contains("x"); }`;
|
|
1788
|
+
const files = compile(src, 'method3');
|
|
1789
|
+
const fn = getFunction(files, 'test');
|
|
1790
|
+
expect(fn).toBeDefined();
|
|
1791
|
+
});
|
|
1792
|
+
it('works with multiple args', () => {
|
|
1793
|
+
const src = `fn test() { let s = set_new(); s.add("a"); s.add("b"); s.remove("a"); }`;
|
|
1794
|
+
const files = compile(src, 'method4');
|
|
1795
|
+
const fn = getFunction(files, 'test');
|
|
1796
|
+
expect(fn).toContain('data remove storage rs:sets __set_0[{value:a}]');
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
describe('Global variables', () => {
|
|
1800
|
+
it('initializes global in __load', () => {
|
|
1801
|
+
const src = `let x: int = 42;\nfn test() { say("hi"); }`;
|
|
1802
|
+
const files = compile(src, 'globaltest');
|
|
1803
|
+
const load = getFunction(files, '__load');
|
|
1804
|
+
expect(load).toContain('scoreboard players set $x rs 42');
|
|
1805
|
+
});
|
|
1806
|
+
it('reads and writes global in function', () => {
|
|
1807
|
+
const src = `let count: int = 0;\nfn inc() { count = count + 1; }`;
|
|
1808
|
+
const files = compile(src, 'globalrw');
|
|
1809
|
+
const fn = getFunction(files, 'inc');
|
|
1810
|
+
expect(fn).toBeDefined();
|
|
1811
|
+
// Global should be initialized in __load
|
|
1812
|
+
const load = getFunction(files, '__load');
|
|
1813
|
+
expect(load).toContain('scoreboard players set $count rs 0');
|
|
1814
|
+
});
|
|
1815
|
+
it('const cannot be reassigned', () => {
|
|
1816
|
+
const src = `const X: int = 5;\nfn bad() { X = 10; }`;
|
|
1817
|
+
expect(() => compile(src, 'constbad')).toThrow();
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
describe('@load decorator', () => {
|
|
1821
|
+
it('calls @load function from __load.mcfunction', () => {
|
|
1822
|
+
const src = `@load fn init() { say("Datapack loaded!"); }`;
|
|
1823
|
+
const files = compile(src, 'loadtest');
|
|
1824
|
+
const load = getFunction(files, '__load');
|
|
1825
|
+
expect(load).toContain('function loadtest:init');
|
|
1826
|
+
});
|
|
1827
|
+
it('calls multiple @load functions in order', () => {
|
|
1828
|
+
const src = `
|
|
1829
|
+
@load fn setup() { say("setup"); }
|
|
1830
|
+
@load fn init() { say("init"); }
|
|
1831
|
+
`;
|
|
1832
|
+
const files = compile(src, 'loadtest');
|
|
1833
|
+
const load = getFunction(files, '__load');
|
|
1834
|
+
const setupIdx = load.indexOf('function loadtest:setup');
|
|
1835
|
+
const initIdx = load.indexOf('function loadtest:init');
|
|
1836
|
+
expect(setupIdx).toBeGreaterThan(-1);
|
|
1837
|
+
expect(initIdx).toBeGreaterThan(-1);
|
|
1838
|
+
expect(setupIdx).toBeLessThan(initIdx);
|
|
1839
|
+
});
|
|
1840
|
+
it('generates the @load function body normally', () => {
|
|
1841
|
+
const src = `@load fn init() { say("hi"); }`;
|
|
1842
|
+
const files = compile(src, 'loadtest');
|
|
1843
|
+
const fn = getFunction(files, 'init');
|
|
1844
|
+
expect(fn).toContain('say hi');
|
|
1845
|
+
});
|
|
1846
|
+
});
|
|
1528
1847
|
//# sourceMappingURL=e2e.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const formatter_1 = require("../formatter");
|
|
4
|
+
describe('formatter', () => {
|
|
5
|
+
it('normalizes indentation to 4 spaces', () => {
|
|
6
|
+
const input = 'fn main() {\n let x: int = 1;\n}';
|
|
7
|
+
const result = (0, formatter_1.format)(input);
|
|
8
|
+
expect(result).toBe('fn main() {\n let x: int = 1;\n}\n');
|
|
9
|
+
});
|
|
10
|
+
it('handles nested blocks', () => {
|
|
11
|
+
const input = 'fn main() {\nif true {\nlet x: int = 1;\n}\n}';
|
|
12
|
+
const result = (0, formatter_1.format)(input);
|
|
13
|
+
expect(result).toBe('fn main() {\n if true {\n let x: int = 1;\n }\n}\n');
|
|
14
|
+
});
|
|
15
|
+
it('trims trailing whitespace', () => {
|
|
16
|
+
const input = 'fn main() { \n let x: int = 1; \n} ';
|
|
17
|
+
const result = (0, formatter_1.format)(input);
|
|
18
|
+
expect(result).toBe('fn main() {\n let x: int = 1;\n}\n');
|
|
19
|
+
});
|
|
20
|
+
it('ensures single newline at end of file', () => {
|
|
21
|
+
const input = 'fn main() {\n}\n\n\n';
|
|
22
|
+
const result = (0, formatter_1.format)(input);
|
|
23
|
+
expect(result).toBe('fn main() {\n}\n');
|
|
24
|
+
});
|
|
25
|
+
it('preserves blank lines', () => {
|
|
26
|
+
const input = 'fn a() {\n}\n\nfn b() {\n}';
|
|
27
|
+
const result = (0, formatter_1.format)(input);
|
|
28
|
+
expect(result).toBe('fn a() {\n}\n\nfn b() {\n}\n');
|
|
29
|
+
});
|
|
30
|
+
it('handles already formatted code', () => {
|
|
31
|
+
const input = 'fn main() {\n let x: int = 1;\n}\n';
|
|
32
|
+
const result = (0, formatter_1.format)(input);
|
|
33
|
+
expect(result).toBe(input);
|
|
34
|
+
});
|
|
35
|
+
it('handles empty input', () => {
|
|
36
|
+
expect((0, formatter_1.format)('')).toBe('\n');
|
|
37
|
+
expect((0, formatter_1.format)('\n\n')).toBe('\n');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=formatter.test.js.map
|
|
@@ -10,10 +10,16 @@ function kinds(tokens) {
|
|
|
10
10
|
describe('Lexer', () => {
|
|
11
11
|
describe('keywords', () => {
|
|
12
12
|
it('recognizes all keywords', () => {
|
|
13
|
-
const tokens = tokenize('fn let const if else while for foreach match return as at in struct enum trigger namespace');
|
|
13
|
+
const tokens = tokenize('fn let const if else while for foreach match return as at in is struct impl enum trigger namespace');
|
|
14
14
|
expect(kinds(tokens)).toEqual([
|
|
15
15
|
'fn', 'let', 'const', 'if', 'else', 'while', 'for', 'foreach', 'match',
|
|
16
|
-
'return', 'as', 'at', 'in', 'struct', 'enum', 'trigger', 'namespace', 'eof'
|
|
16
|
+
'return', 'as', 'at', 'in', 'is', 'struct', 'impl', 'enum', 'trigger', 'namespace', 'eof'
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
it('tokenizes is-check and impl syntax with their dedicated keywords', () => {
|
|
20
|
+
const tokens = tokenize('if (e is Player) { } impl Point { }');
|
|
21
|
+
expect(kinds(tokens)).toEqual([
|
|
22
|
+
'if', '(', 'ident', 'is', 'ident', ')', '{', '}', 'impl', 'ident', '{', '}', 'eof',
|
|
17
23
|
]);
|
|
18
24
|
});
|
|
19
25
|
it('recognizes type keywords', () => {
|
|
@@ -169,6 +175,10 @@ describe('Lexer', () => {
|
|
|
169
175
|
const tokens = tokenize('=>');
|
|
170
176
|
expect(kinds(tokens)).toEqual(['=>', 'eof']);
|
|
171
177
|
});
|
|
178
|
+
it('tokenizes static method separators for impl methods', () => {
|
|
179
|
+
const tokens = tokenize('Point::new()');
|
|
180
|
+
expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof']);
|
|
181
|
+
});
|
|
172
182
|
});
|
|
173
183
|
describe('delimiters', () => {
|
|
174
184
|
it('tokenizes all delimiters', () => {
|