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
|
@@ -23,6 +23,7 @@ describe('Parser', () => {
|
|
|
23
23
|
const program = parse('');
|
|
24
24
|
expect(program.namespace).toBe('test');
|
|
25
25
|
expect(program.declarations).toEqual([]);
|
|
26
|
+
expect(program.implBlocks).toEqual([]);
|
|
26
27
|
expect(program.enums).toEqual([]);
|
|
27
28
|
expect(program.consts).toEqual([]);
|
|
28
29
|
});
|
|
@@ -90,6 +91,12 @@ describe('Parser', () => {
|
|
|
90
91
|
{ name: 'on_death' },
|
|
91
92
|
]);
|
|
92
93
|
});
|
|
94
|
+
it('parses @on event decorators', () => {
|
|
95
|
+
const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}');
|
|
96
|
+
expect(program.declarations[0].decorators).toEqual([
|
|
97
|
+
{ name: 'on', args: { eventType: 'PlayerDeath' } },
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
93
100
|
});
|
|
94
101
|
describe('types', () => {
|
|
95
102
|
it('parses primitive types', () => {
|
|
@@ -134,6 +141,50 @@ describe('Parser', () => {
|
|
|
134
141
|
},
|
|
135
142
|
]);
|
|
136
143
|
});
|
|
144
|
+
it('parses impl blocks', () => {
|
|
145
|
+
const program = parse(`
|
|
146
|
+
struct Timer { duration: int }
|
|
147
|
+
|
|
148
|
+
impl Timer {
|
|
149
|
+
fn new(duration: int): Timer {
|
|
150
|
+
return { duration: duration };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn start(self) {}
|
|
154
|
+
}
|
|
155
|
+
`);
|
|
156
|
+
expect(program.implBlocks).toHaveLength(1);
|
|
157
|
+
expect(program.implBlocks[0].typeName).toBe('Timer');
|
|
158
|
+
expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start']);
|
|
159
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
160
|
+
name: 'self',
|
|
161
|
+
type: { kind: 'struct', name: 'Timer' },
|
|
162
|
+
default: undefined,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it('parses impl blocks with static and instance methods', () => {
|
|
166
|
+
const program = parse(`
|
|
167
|
+
struct Point { x: int, y: int }
|
|
168
|
+
|
|
169
|
+
impl Point {
|
|
170
|
+
fn new(x: int, y: int) -> Point {
|
|
171
|
+
return { x: x, y: y };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn distance(self) -> int {
|
|
175
|
+
return self.x + self.y;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`);
|
|
179
|
+
expect(program.implBlocks).toHaveLength(1);
|
|
180
|
+
expect(program.implBlocks[0].typeName).toBe('Point');
|
|
181
|
+
expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y']);
|
|
182
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
183
|
+
name: 'self',
|
|
184
|
+
type: { kind: 'struct', name: 'Point' },
|
|
185
|
+
default: undefined,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
137
188
|
});
|
|
138
189
|
describe('statements', () => {
|
|
139
190
|
it('parses let statement', () => {
|
|
@@ -173,6 +224,26 @@ describe('Parser', () => {
|
|
|
173
224
|
expect(stmt.kind).toBe('if');
|
|
174
225
|
expect(stmt.else_).toHaveLength(1);
|
|
175
226
|
});
|
|
227
|
+
it('parses entity is-checks in if conditions', () => {
|
|
228
|
+
const stmt = parseStmt('if (e is Player) { kill(@s); }');
|
|
229
|
+
expect(stmt.kind).toBe('if');
|
|
230
|
+
expect(stmt.cond).toEqual({
|
|
231
|
+
kind: 'is_check',
|
|
232
|
+
expr: { kind: 'ident', name: 'e' },
|
|
233
|
+
entityType: 'Player',
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it('parses entity is-checks inside foreach bodies', () => {
|
|
237
|
+
const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }');
|
|
238
|
+
expect(stmt.kind).toBe('foreach');
|
|
239
|
+
const innerIf = stmt.body[0];
|
|
240
|
+
expect(innerIf.kind).toBe('if');
|
|
241
|
+
expect(innerIf.cond).toEqual({
|
|
242
|
+
kind: 'is_check',
|
|
243
|
+
expr: { kind: 'ident', name: 'e' },
|
|
244
|
+
entityType: 'Zombie',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
176
247
|
it('parses while statement', () => {
|
|
177
248
|
const stmt = parseStmt('while (i > 0) { i = i - 1; }');
|
|
178
249
|
expect(stmt.kind).toBe('while');
|
|
@@ -429,6 +500,15 @@ describe('Parser', () => {
|
|
|
429
500
|
});
|
|
430
501
|
});
|
|
431
502
|
});
|
|
503
|
+
it('parses static method calls', () => {
|
|
504
|
+
const expr = parseExpr('Timer::new(100)');
|
|
505
|
+
expect(expr).toEqual({
|
|
506
|
+
kind: 'static_call',
|
|
507
|
+
type: 'Timer',
|
|
508
|
+
method: 'new',
|
|
509
|
+
args: [{ kind: 'int_lit', value: 100 }],
|
|
510
|
+
});
|
|
511
|
+
});
|
|
432
512
|
describe('binary operators', () => {
|
|
433
513
|
it('parses arithmetic', () => {
|
|
434
514
|
const expr = parseExpr('1 + 2');
|
|
@@ -58,7 +58,7 @@ function loadExample(name) {
|
|
|
58
58
|
}
|
|
59
59
|
describe('MCRuntime behavioral integration', () => {
|
|
60
60
|
it('runs the counter example and increments the scoreboard across ticks', () => {
|
|
61
|
-
const runtime = loadCompiledProgram(loadExample('counter.
|
|
61
|
+
const runtime = loadCompiledProgram(loadExample('counter.mcrs'));
|
|
62
62
|
runtime.load();
|
|
63
63
|
runtime.ticks(5);
|
|
64
64
|
expect(runtime.getScore('counter', 'ticks')).toBe(5);
|
|
@@ -81,7 +81,7 @@ fn compute() {
|
|
|
81
81
|
`);
|
|
82
82
|
runtime.load();
|
|
83
83
|
runtime.execFunction('compute');
|
|
84
|
-
expect(runtime.getScore('math', 'result')).toBe(11);
|
|
84
|
+
expect(runtime.getScore('math', 'runtime.result')).toBe(11);
|
|
85
85
|
});
|
|
86
86
|
it('captures say, announce, actionbar, and title output in the chat log', () => {
|
|
87
87
|
const runtime = loadCompiledProgram(`
|
|
@@ -146,8 +146,8 @@ fn arrays() {
|
|
|
146
146
|
`);
|
|
147
147
|
runtime.load();
|
|
148
148
|
runtime.execFunction('arrays');
|
|
149
|
-
expect(runtime.getScore('arrays', 'len')).toBe(1);
|
|
150
|
-
expect(runtime.getScore('arrays', 'last')).toBe(9);
|
|
149
|
+
expect(runtime.getScore('arrays', 'runtime.len')).toBe(1);
|
|
150
|
+
expect(runtime.getScore('arrays', 'runtime.last')).toBe(9);
|
|
151
151
|
expect(runtime.getStorage('rs:heap.arr')).toEqual([4]);
|
|
152
152
|
});
|
|
153
153
|
it('tracks world state, weather, and time from compiled world commands', () => {
|
|
@@ -182,7 +182,7 @@ fn pulse() {
|
|
|
182
182
|
`);
|
|
183
183
|
runtime.load();
|
|
184
184
|
runtime.ticks(10);
|
|
185
|
-
expect(runtime.getScore('pulse', 'count')).toBe(2);
|
|
185
|
+
expect(runtime.getScore('pulse', 'runtime.count')).toBe(2);
|
|
186
186
|
});
|
|
187
187
|
it('executes only the matching match arm', () => {
|
|
188
188
|
const runtime = loadCompiledProgram(`
|
|
@@ -229,7 +229,7 @@ fn test() {
|
|
|
229
229
|
`);
|
|
230
230
|
runtime.load();
|
|
231
231
|
runtime.execFunction('test');
|
|
232
|
-
expect(runtime.getScore('lambda', 'direct')).toBe(10);
|
|
232
|
+
expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10);
|
|
233
233
|
});
|
|
234
234
|
it('executes lambdas passed as callback arguments', () => {
|
|
235
235
|
const runtime = loadCompiledProgram(`
|
|
@@ -244,7 +244,7 @@ fn test() {
|
|
|
244
244
|
`);
|
|
245
245
|
runtime.load();
|
|
246
246
|
runtime.execFunction('test');
|
|
247
|
-
expect(runtime.getScore('lambda', 'callback')).toBe(15);
|
|
247
|
+
expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15);
|
|
248
248
|
});
|
|
249
249
|
it('executes block-body lambdas', () => {
|
|
250
250
|
const runtime = loadCompiledProgram(`
|
|
@@ -259,7 +259,7 @@ fn test() {
|
|
|
259
259
|
`);
|
|
260
260
|
runtime.load();
|
|
261
261
|
runtime.execFunction('test');
|
|
262
|
-
expect(runtime.getScore('lambda', 'block')).toBe(11);
|
|
262
|
+
expect(runtime.getScore('lambda', 'runtime.block')).toBe(11);
|
|
263
263
|
});
|
|
264
264
|
it('executes immediately-invoked expression-body lambdas', () => {
|
|
265
265
|
const runtime = loadCompiledProgram(`
|
|
@@ -270,7 +270,7 @@ fn test() {
|
|
|
270
270
|
`);
|
|
271
271
|
runtime.load();
|
|
272
272
|
runtime.execFunction('test');
|
|
273
|
-
expect(runtime.getScore('lambda', 'iife')).toBe(10);
|
|
273
|
+
expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10);
|
|
274
274
|
});
|
|
275
275
|
});
|
|
276
276
|
//# sourceMappingURL=runtime.test.js.map
|
|
@@ -193,6 +193,137 @@ fn test() {
|
|
|
193
193
|
`);
|
|
194
194
|
expect(errors).toHaveLength(0);
|
|
195
195
|
});
|
|
196
|
+
it('type checks timer builtins with void callbacks and interval IDs', () => {
|
|
197
|
+
const errors = typeCheck(`
|
|
198
|
+
fn test() {
|
|
199
|
+
setTimeout(100, () => {
|
|
200
|
+
say("later");
|
|
201
|
+
});
|
|
202
|
+
let intervalId: int = setInterval(20, () => {
|
|
203
|
+
say("tick");
|
|
204
|
+
});
|
|
205
|
+
clearInterval(intervalId);
|
|
206
|
+
}
|
|
207
|
+
`);
|
|
208
|
+
expect(errors).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
it('rejects timer callbacks with the wrong return type', () => {
|
|
211
|
+
const errors = typeCheck(`
|
|
212
|
+
fn test() {
|
|
213
|
+
setTimeout(100, () => 1);
|
|
214
|
+
}
|
|
215
|
+
`);
|
|
216
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
217
|
+
expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
|
|
218
|
+
});
|
|
219
|
+
it('allows impl instance methods with inferred self type', () => {
|
|
220
|
+
const errors = typeCheck(`
|
|
221
|
+
struct Timer { duration: int }
|
|
222
|
+
|
|
223
|
+
impl Timer {
|
|
224
|
+
fn elapsed(self) -> int {
|
|
225
|
+
return self.duration;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn test() {
|
|
230
|
+
let timer: Timer = { duration: 10 };
|
|
231
|
+
let value: int = timer.elapsed();
|
|
232
|
+
}
|
|
233
|
+
`);
|
|
234
|
+
expect(errors).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
it('records then-branch entity narrowing for is-checks', () => {
|
|
237
|
+
const source = `
|
|
238
|
+
fn test() {
|
|
239
|
+
foreach (e in @e) {
|
|
240
|
+
if (e is Player) {
|
|
241
|
+
kill(e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
const tokens = new lexer_1.Lexer(source).tokenize();
|
|
247
|
+
const ast = new parser_1.Parser(tokens).parse('test');
|
|
248
|
+
const checker = new typechecker_1.TypeChecker(source);
|
|
249
|
+
checker.check(ast);
|
|
250
|
+
const foreachStmt = ast.declarations[0].body[0];
|
|
251
|
+
const ifStmt = foreachStmt.body[0];
|
|
252
|
+
expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
|
|
253
|
+
name: 'e',
|
|
254
|
+
type: { kind: 'entity', entityType: 'Player' },
|
|
255
|
+
mutable: false,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
it('allows static impl method calls', () => {
|
|
259
|
+
const errors = typeCheck(`
|
|
260
|
+
struct Timer { duration: int }
|
|
261
|
+
|
|
262
|
+
impl Timer {
|
|
263
|
+
fn new(duration: int) -> Timer {
|
|
264
|
+
return { duration: duration };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn test() {
|
|
269
|
+
let timer: Timer = Timer::new(10);
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
expect(errors).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
it('rejects using is-checks on non-entity values', () => {
|
|
275
|
+
const errors = typeCheck(`
|
|
276
|
+
fn test() {
|
|
277
|
+
let x: int = 1;
|
|
278
|
+
if (x is Player) {
|
|
279
|
+
say("nope");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`);
|
|
283
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
284
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression, got int");
|
|
285
|
+
});
|
|
286
|
+
it('rejects calling instance impl methods as static methods', () => {
|
|
287
|
+
const errors = typeCheck(`
|
|
288
|
+
struct Point { x: int, y: int }
|
|
289
|
+
|
|
290
|
+
impl Point {
|
|
291
|
+
fn distance(self) -> int {
|
|
292
|
+
return self.x + self.y;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn test() {
|
|
297
|
+
let total: int = Point::distance();
|
|
298
|
+
}
|
|
299
|
+
`);
|
|
300
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
301
|
+
expect(errors[0].message).toContain("Method 'Point::distance' is an instance method");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('entity is-check narrowing', () => {
|
|
305
|
+
it('allows entity type checks on foreach bindings', () => {
|
|
306
|
+
const errors = typeCheck(`
|
|
307
|
+
fn test() {
|
|
308
|
+
foreach (e in @e) {
|
|
309
|
+
if (e is Player) {
|
|
310
|
+
kill(@s);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
`);
|
|
315
|
+
expect(errors).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
it('rejects is-checks on non-entity expressions', () => {
|
|
318
|
+
const errors = typeCheck(`
|
|
319
|
+
fn test() {
|
|
320
|
+
let x: int = 1;
|
|
321
|
+
if (x is Player) {}
|
|
322
|
+
}
|
|
323
|
+
`);
|
|
324
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
325
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression");
|
|
326
|
+
});
|
|
196
327
|
});
|
|
197
328
|
describe('return type checking', () => {
|
|
198
329
|
it('allows matching return type', () => {
|
|
@@ -360,5 +491,32 @@ fn broken() -> int {
|
|
|
360
491
|
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
361
492
|
});
|
|
362
493
|
});
|
|
494
|
+
describe('event handlers', () => {
|
|
495
|
+
it('accepts matching @on event signatures', () => {
|
|
496
|
+
const errors = typeCheck(`
|
|
497
|
+
@on(PlayerDeath)
|
|
498
|
+
fn handle_death(player: Player) {
|
|
499
|
+
tp(player, @p);
|
|
500
|
+
}
|
|
501
|
+
`);
|
|
502
|
+
expect(errors).toHaveLength(0);
|
|
503
|
+
});
|
|
504
|
+
it('rejects unknown event types', () => {
|
|
505
|
+
const errors = typeCheck(`
|
|
506
|
+
@on(NotARealEvent)
|
|
507
|
+
fn handle(player: Player) {}
|
|
508
|
+
`);
|
|
509
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
510
|
+
expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'");
|
|
511
|
+
});
|
|
512
|
+
it('rejects mismatched event signatures', () => {
|
|
513
|
+
const errors = typeCheck(`
|
|
514
|
+
@on(BlockBreak)
|
|
515
|
+
fn handle_break(player: Player) {}
|
|
516
|
+
`);
|
|
517
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
518
|
+
expect(errors[0].message).toContain('must declare 2 parameter(s)');
|
|
519
|
+
});
|
|
520
|
+
});
|
|
363
521
|
});
|
|
364
522
|
//# sourceMappingURL=typechecker.test.js.map
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface Span {
|
|
|
12
12
|
endCol?: number;
|
|
13
13
|
}
|
|
14
14
|
export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double';
|
|
15
|
+
export type EntityTypeName = 'entity' | 'Player' | 'Mob' | 'HostileMob' | 'PassiveMob' | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman' | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager' | 'ArmorStand' | 'Item' | 'Arrow';
|
|
15
16
|
export type TypeNode = {
|
|
16
17
|
kind: 'named';
|
|
17
18
|
name: PrimitiveType;
|
|
@@ -28,6 +29,11 @@ export type TypeNode = {
|
|
|
28
29
|
kind: 'function_type';
|
|
29
30
|
params: TypeNode[];
|
|
30
31
|
return: TypeNode;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'entity';
|
|
34
|
+
entityType: EntityTypeName;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'selector';
|
|
31
37
|
};
|
|
32
38
|
export interface LambdaParam {
|
|
33
39
|
name: string;
|
|
@@ -54,6 +60,11 @@ export interface SelectorFilter {
|
|
|
54
60
|
sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary';
|
|
55
61
|
nbt?: string;
|
|
56
62
|
gamemode?: string;
|
|
63
|
+
x?: RangeExpr;
|
|
64
|
+
y?: RangeExpr;
|
|
65
|
+
z?: RangeExpr;
|
|
66
|
+
x_rotation?: RangeExpr;
|
|
67
|
+
y_rotation?: RangeExpr;
|
|
57
68
|
}
|
|
58
69
|
export interface EntitySelector {
|
|
59
70
|
kind: SelectorKind;
|
|
@@ -138,6 +149,11 @@ export type Expr = {
|
|
|
138
149
|
left: Expr;
|
|
139
150
|
right: Expr;
|
|
140
151
|
span?: Span;
|
|
152
|
+
} | {
|
|
153
|
+
kind: 'is_check';
|
|
154
|
+
expr: Expr;
|
|
155
|
+
entityType: EntityTypeName;
|
|
156
|
+
span?: Span;
|
|
141
157
|
} | {
|
|
142
158
|
kind: 'unary';
|
|
143
159
|
op: '!' | '-';
|
|
@@ -213,10 +229,14 @@ export type ExecuteSubcommand = {
|
|
|
213
229
|
selector: EntitySelector;
|
|
214
230
|
} | {
|
|
215
231
|
kind: 'if_entity';
|
|
216
|
-
selector
|
|
232
|
+
selector?: EntitySelector;
|
|
233
|
+
varName?: string;
|
|
234
|
+
filters?: SelectorFilter;
|
|
217
235
|
} | {
|
|
218
236
|
kind: 'unless_entity';
|
|
219
|
-
selector
|
|
237
|
+
selector?: EntitySelector;
|
|
238
|
+
varName?: string;
|
|
239
|
+
filters?: SelectorFilter;
|
|
220
240
|
} | {
|
|
221
241
|
kind: 'in';
|
|
222
242
|
dimension: string;
|
|
@@ -302,9 +322,10 @@ export type Stmt = {
|
|
|
302
322
|
};
|
|
303
323
|
export type Block = Stmt[];
|
|
304
324
|
export interface Decorator {
|
|
305
|
-
name: 'tick' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
325
|
+
name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
306
326
|
args?: {
|
|
307
327
|
rate?: number;
|
|
328
|
+
eventType?: string;
|
|
308
329
|
trigger?: string;
|
|
309
330
|
advancement?: string;
|
|
310
331
|
item?: string;
|
|
@@ -333,6 +354,12 @@ export interface StructDecl {
|
|
|
333
354
|
fields: StructField[];
|
|
334
355
|
span?: Span;
|
|
335
356
|
}
|
|
357
|
+
export interface ImplBlock {
|
|
358
|
+
kind: 'impl_block';
|
|
359
|
+
typeName: string;
|
|
360
|
+
methods: FnDecl[];
|
|
361
|
+
span?: Span;
|
|
362
|
+
}
|
|
336
363
|
export interface EnumVariant {
|
|
337
364
|
name: string;
|
|
338
365
|
value?: number;
|
|
@@ -348,10 +375,20 @@ export interface ConstDecl {
|
|
|
348
375
|
value: LiteralExpr;
|
|
349
376
|
span?: Span;
|
|
350
377
|
}
|
|
378
|
+
export interface GlobalDecl {
|
|
379
|
+
kind: 'global';
|
|
380
|
+
name: string;
|
|
381
|
+
type: TypeNode;
|
|
382
|
+
init: Expr;
|
|
383
|
+
mutable: boolean;
|
|
384
|
+
span?: Span;
|
|
385
|
+
}
|
|
351
386
|
export interface Program {
|
|
352
387
|
namespace: string;
|
|
388
|
+
globals: GlobalDecl[];
|
|
353
389
|
declarations: FnDecl[];
|
|
354
390
|
structs: StructDecl[];
|
|
391
|
+
implBlocks: ImplBlock[];
|
|
355
392
|
enums: EnumDecl[];
|
|
356
393
|
consts: ConstDecl[];
|
|
357
394
|
}
|
package/dist/cli.js
CHANGED
|
@@ -60,13 +60,15 @@ Usage:
|
|
|
60
60
|
redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
|
|
61
61
|
redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
|
|
62
62
|
redscript check <file>
|
|
63
|
+
redscript fmt <file.mcrs> [file2.mcrs ...]
|
|
63
64
|
redscript repl
|
|
64
65
|
redscript version
|
|
65
66
|
|
|
66
67
|
Commands:
|
|
67
68
|
compile Compile a RedScript file to a Minecraft datapack
|
|
68
|
-
watch Watch a directory for .
|
|
69
|
+
watch Watch a directory for .mcrs file changes, recompile, and hot reload
|
|
69
70
|
check Check a RedScript file for errors without generating output
|
|
71
|
+
fmt Auto-format RedScript source files
|
|
70
72
|
repl Start an interactive RedScript REPL
|
|
71
73
|
version Print the RedScript version
|
|
72
74
|
|
|
@@ -275,18 +277,18 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
|
275
277
|
console.error(`Error: ${dir} is not a directory`);
|
|
276
278
|
process.exit(1);
|
|
277
279
|
}
|
|
278
|
-
console.log(`👁 Watching ${dir} for .
|
|
280
|
+
console.log(`👁 Watching ${dir} for .mcrs file changes...`);
|
|
279
281
|
console.log(` Output: ${output}`);
|
|
280
282
|
if (hotReloadUrl)
|
|
281
283
|
console.log(` Hot reload: ${hotReloadUrl}`);
|
|
282
284
|
console.log(` Press Ctrl+C to stop\n`);
|
|
283
285
|
// Debounce timer
|
|
284
286
|
let debounceTimer = null;
|
|
285
|
-
// Compile all .
|
|
287
|
+
// Compile all .mcrs files in directory
|
|
286
288
|
async function compileAll() {
|
|
287
289
|
const files = findRsFiles(dir);
|
|
288
290
|
if (files.length === 0) {
|
|
289
|
-
console.log(`⚠ No .
|
|
291
|
+
console.log(`⚠ No .mcrs files found in ${dir}`);
|
|
290
292
|
return;
|
|
291
293
|
}
|
|
292
294
|
let hasErrors = false;
|
|
@@ -321,7 +323,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
|
321
323
|
console.log('');
|
|
322
324
|
}
|
|
323
325
|
}
|
|
324
|
-
// Find all .
|
|
326
|
+
// Find all .mcrs files recursively
|
|
325
327
|
function findRsFiles(directory) {
|
|
326
328
|
const results = [];
|
|
327
329
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
@@ -330,7 +332,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
|
330
332
|
if (entry.isDirectory()) {
|
|
331
333
|
results.push(...findRsFiles(fullPath));
|
|
332
334
|
}
|
|
333
|
-
else if (entry.isFile() && entry.name.endsWith('.
|
|
335
|
+
else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
|
|
334
336
|
results.push(fullPath);
|
|
335
337
|
}
|
|
336
338
|
}
|
|
@@ -340,7 +342,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
|
340
342
|
void compileAll();
|
|
341
343
|
// Watch for changes
|
|
342
344
|
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
343
|
-
if (filename && filename.endsWith('.
|
|
345
|
+
if (filename && filename.endsWith('.mcrs')) {
|
|
344
346
|
// Debounce rapid changes
|
|
345
347
|
if (debounceTimer) {
|
|
346
348
|
clearTimeout(debounceTimer);
|
|
@@ -391,6 +393,22 @@ async function main() {
|
|
|
391
393
|
}
|
|
392
394
|
checkCommand(parsed.file);
|
|
393
395
|
break;
|
|
396
|
+
case 'fmt':
|
|
397
|
+
case 'format': {
|
|
398
|
+
const files = args.filter(a => a.endsWith('.mcrs'));
|
|
399
|
+
if (files.length === 0) {
|
|
400
|
+
console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const { format } = require('./formatter');
|
|
404
|
+
for (const file of files) {
|
|
405
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
406
|
+
const formatted = format(content);
|
|
407
|
+
fs.writeFileSync(file, formatted);
|
|
408
|
+
console.log(`Formatted: ${file}`);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
394
412
|
case 'repl':
|
|
395
413
|
await (0, repl_1.startRepl)(parsed.namespace ?? 'repl');
|
|
396
414
|
break;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Variable mapping:
|
|
13
13
|
* scoreboard objective: "rs"
|
|
14
14
|
* fake player: "$<varname>"
|
|
15
|
-
* temporaries: "$
|
|
15
|
+
* temporaries: "$_0", "$_1", ...
|
|
16
16
|
* return value: "$ret"
|
|
17
17
|
* parameters: "$p0", "$p1", ...
|
|
18
18
|
*/
|
|
@@ -21,6 +21,7 @@ exports.countMcfunctionCommands = countMcfunctionCommands;
|
|
|
21
21
|
exports.generateDatapackWithStats = generateDatapackWithStats;
|
|
22
22
|
exports.generateDatapack = generateDatapack;
|
|
23
23
|
const commands_1 = require("../../optimizer/commands");
|
|
24
|
+
const types_1 = require("../../events/types");
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Utilities
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -238,6 +239,8 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
238
239
|
// Collect all trigger handlers
|
|
239
240
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
|
|
240
241
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
|
|
242
|
+
const eventHandlers = module.functions.filter((fn) => !!fn.eventHandler && (0, types_1.isEventTypeName)(fn.eventHandler.eventType));
|
|
243
|
+
const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
|
|
241
244
|
// Collect all tick functions
|
|
242
245
|
const tickFunctionNames = [];
|
|
243
246
|
for (const fn of module.functions) {
|
|
@@ -258,13 +261,28 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
258
261
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
259
262
|
];
|
|
260
263
|
for (const g of module.globals) {
|
|
261
|
-
loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ}
|
|
264
|
+
loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`);
|
|
262
265
|
}
|
|
263
266
|
// Add trigger objectives
|
|
264
267
|
for (const triggerName of triggerNames) {
|
|
265
268
|
loadLines.push(`scoreboard objectives add ${triggerName} trigger`);
|
|
266
269
|
loadLines.push(`scoreboard players enable @a ${triggerName}`);
|
|
267
270
|
}
|
|
271
|
+
for (const eventType of eventTypes) {
|
|
272
|
+
const detection = types_1.EVENT_TYPES[eventType].detection;
|
|
273
|
+
if (eventType === 'PlayerDeath') {
|
|
274
|
+
loadLines.push('scoreboard objectives add rs.deaths deathCount');
|
|
275
|
+
}
|
|
276
|
+
else if (eventType === 'EntityKill') {
|
|
277
|
+
loadLines.push('scoreboard objectives add rs.kills totalKillCount');
|
|
278
|
+
}
|
|
279
|
+
else if (eventType === 'ItemUse') {
|
|
280
|
+
loadLines.push('# ItemUse detection requires a project-specific objective/tag setup');
|
|
281
|
+
}
|
|
282
|
+
else if (detection === 'tag' || detection === 'advancement') {
|
|
283
|
+
loadLines.push(`# ${eventType} detection expects tag ${types_1.EVENT_TYPES[eventType].tag} to be set externally`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
268
286
|
// Generate trigger dispatch functions
|
|
269
287
|
for (const triggerName of triggerNames) {
|
|
270
288
|
const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName);
|
|
@@ -310,6 +328,12 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
310
328
|
files.push({ path: filePath, content: lines.join('\n') });
|
|
311
329
|
}
|
|
312
330
|
}
|
|
331
|
+
// Call @load functions from __load
|
|
332
|
+
for (const fn of module.functions) {
|
|
333
|
+
if (fn.isLoadInit) {
|
|
334
|
+
loadLines.push(`function ${ns}:${fn.name}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
313
337
|
// Write __load.mcfunction
|
|
314
338
|
files.push({
|
|
315
339
|
path: `data/${ns}/function/__load.mcfunction`,
|
|
@@ -333,8 +357,19 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
333
357
|
tickLines.push(`execute as @a[scores={${triggerName}=1..}] run function ${ns}:__trigger_${triggerName}_dispatch`);
|
|
334
358
|
}
|
|
335
359
|
}
|
|
360
|
+
if (eventHandlers.length > 0) {
|
|
361
|
+
tickLines.push('# Event checks');
|
|
362
|
+
for (const eventType of eventTypes) {
|
|
363
|
+
const tag = types_1.EVENT_TYPES[eventType].tag;
|
|
364
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType);
|
|
365
|
+
for (const handler of handlers) {
|
|
366
|
+
tickLines.push(`execute as @a[tag=${tag}] run function ${ns}:${handler.name}`);
|
|
367
|
+
}
|
|
368
|
+
tickLines.push(`tag @a[tag=${tag}] remove ${tag}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
336
371
|
// Only generate __tick if there's something to run
|
|
337
|
-
if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
|
|
372
|
+
if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
|
|
338
373
|
files.push({
|
|
339
374
|
path: `data/${ns}/function/__tick.mcfunction`,
|
|
340
375
|
content: tickLines.join('\n'),
|