redscript-mc 1.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/cli.test.js +138 -0
  3. package/dist/__tests__/codegen.test.js +25 -0
  4. package/dist/__tests__/e2e.test.js +190 -12
  5. package/dist/__tests__/lexer.test.js +12 -2
  6. package/dist/__tests__/lowering.test.js +164 -9
  7. package/dist/__tests__/mc-integration.test.js +145 -51
  8. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  9. package/dist/__tests__/parser.test.js +80 -0
  10. package/dist/__tests__/runtime.test.js +8 -8
  11. package/dist/__tests__/typechecker.test.js +158 -0
  12. package/dist/ast/types.d.ts +20 -1
  13. package/dist/codegen/mcfunction/index.js +30 -1
  14. package/dist/codegen/structure/index.js +25 -0
  15. package/dist/compile.d.ts +10 -0
  16. package/dist/compile.js +36 -5
  17. package/dist/events/types.d.ts +35 -0
  18. package/dist/events/types.js +59 -0
  19. package/dist/index.js +3 -2
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +2 -0
  23. package/dist/lowering/index.d.ts +32 -1
  24. package/dist/lowering/index.js +439 -15
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +79 -10
  27. package/dist/typechecker/index.d.ts +17 -0
  28. package/dist/typechecker/index.js +343 -17
  29. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  30. package/editors/vscode/CHANGELOG.md +9 -0
  31. package/editors/vscode/out/extension.js +1144 -72
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +166 -0
  36. package/src/__tests__/codegen.test.ts +27 -0
  37. package/src/__tests__/e2e.test.ts +201 -12
  38. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  39. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  40. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  41. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  42. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  43. package/src/__tests__/lexer.test.ts +14 -2
  44. package/src/__tests__/lowering.test.ts +178 -9
  45. package/src/__tests__/mc-integration.test.ts +166 -51
  46. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  47. package/src/__tests__/parser.test.ts +91 -5
  48. package/src/__tests__/runtime.test.ts +8 -8
  49. package/src/__tests__/typechecker.test.ts +171 -0
  50. package/src/ast/types.ts +25 -1
  51. package/src/codegen/mcfunction/index.ts +31 -1
  52. package/src/codegen/structure/index.ts +27 -0
  53. package/src/compile.ts +54 -6
  54. package/src/events/types.ts +69 -0
  55. package/src/index.ts +4 -3
  56. package/src/ir/types.ts +4 -0
  57. package/src/lexer/index.ts +3 -1
  58. package/src/lowering/index.ts +528 -16
  59. package/src/parser/index.ts +90 -12
  60. package/src/stdlib/README.md +34 -4
  61. package/src/stdlib/tags.mcrs +951 -0
  62. package/src/stdlib/timer.mcrs +54 -33
  63. package/src/typechecker/index.ts +404 -18
@@ -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');
@@ -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
@@ -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;
@@ -143,6 +149,11 @@ export type Expr = {
143
149
  left: Expr;
144
150
  right: Expr;
145
151
  span?: Span;
152
+ } | {
153
+ kind: 'is_check';
154
+ expr: Expr;
155
+ entityType: EntityTypeName;
156
+ span?: Span;
146
157
  } | {
147
158
  kind: 'unary';
148
159
  op: '!' | '-';
@@ -311,9 +322,10 @@ export type Stmt = {
311
322
  };
312
323
  export type Block = Stmt[];
313
324
  export interface Decorator {
314
- name: 'tick' | 'load' | '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';
315
326
  args?: {
316
327
  rate?: number;
328
+ eventType?: string;
317
329
  trigger?: string;
318
330
  advancement?: string;
319
331
  item?: string;
@@ -342,6 +354,12 @@ export interface StructDecl {
342
354
  fields: StructField[];
343
355
  span?: Span;
344
356
  }
357
+ export interface ImplBlock {
358
+ kind: 'impl_block';
359
+ typeName: string;
360
+ methods: FnDecl[];
361
+ span?: Span;
362
+ }
345
363
  export interface EnumVariant {
346
364
  name: string;
347
365
  value?: number;
@@ -370,6 +388,7 @@ export interface Program {
370
388
  globals: GlobalDecl[];
371
389
  declarations: FnDecl[];
372
390
  structs: StructDecl[];
391
+ implBlocks: ImplBlock[];
373
392
  enums: EnumDecl[];
374
393
  consts: ConstDecl[];
375
394
  }
@@ -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) {
@@ -265,6 +268,21 @@ function generateDatapackWithStats(module, options = {}) {
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);
@@ -339,8 +357,19 @@ function generateDatapackWithStats(module, options = {}) {
339
357
  tickLines.push(`execute as @a[scores={${triggerName}=1..}] run function ${ns}:__trigger_${triggerName}_dispatch`);
340
358
  }
341
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
+ }
342
371
  // Only generate __tick if there's something to run
343
- if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
372
+ if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
344
373
  files.push({
345
374
  path: `data/${ns}/function/__tick.mcfunction`,
346
375
  content: tickLines.join('\n'),
@@ -10,6 +10,7 @@ const commands_1 = require("../../optimizer/commands");
10
10
  const passes_1 = require("../../optimizer/passes");
11
11
  const structure_1 = require("../../optimizer/structure");
12
12
  const compile_1 = require("../../compile");
13
+ const types_1 = require("../../events/types");
13
14
  const DATA_VERSION = 3953;
14
15
  const MAX_WIDTH = 16;
15
16
  const OBJ = 'rs';
@@ -62,6 +63,8 @@ function collectCommandEntriesFromModule(module) {
62
63
  const entries = [];
63
64
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
64
65
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
66
+ const eventHandlers = module.functions.filter((fn) => !!fn.eventHandler && (0, types_1.isEventTypeName)(fn.eventHandler.eventType));
67
+ const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
65
68
  const loadCommands = [
66
69
  `scoreboard objectives add ${OBJ} dummy`,
67
70
  ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
@@ -71,6 +74,14 @@ function collectCommandEntriesFromModule(module) {
71
74
  ]),
72
75
  ...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(constSetup),
73
76
  ];
77
+ for (const eventType of eventTypes) {
78
+ if (eventType === 'PlayerDeath') {
79
+ loadCommands.push('scoreboard objectives add rs.deaths deathCount');
80
+ }
81
+ else if (eventType === 'EntityKill') {
82
+ loadCommands.push('scoreboard objectives add rs.kills totalKillCount');
83
+ }
84
+ }
74
85
  // Call @load functions from __load
75
86
  for (const fn of module.functions) {
76
87
  if (fn.isLoadInit) {
@@ -114,6 +125,20 @@ function collectCommandEntriesFromModule(module) {
114
125
  });
115
126
  }
116
127
  }
128
+ if (eventHandlers.length > 0) {
129
+ for (const eventType of eventTypes) {
130
+ const tag = types_1.EVENT_TYPES[eventType].tag;
131
+ const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType);
132
+ for (const handler of handlers) {
133
+ tickCommands.push({
134
+ cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
135
+ });
136
+ }
137
+ tickCommands.push({
138
+ cmd: `tag @a[tag=${tag}] remove ${tag}`,
139
+ });
140
+ }
141
+ }
117
142
  if (tickCommands.length > 0) {
118
143
  sections.push({
119
144
  name: '__tick',
package/dist/compile.d.ts CHANGED
@@ -20,10 +20,20 @@ export interface CompileResult {
20
20
  ir?: IRModule;
21
21
  error?: DiagnosticError;
22
22
  }
23
+ export interface SourceRange {
24
+ startLine: number;
25
+ endLine: number;
26
+ filePath: string;
27
+ }
28
+ export interface PreprocessedSource {
29
+ source: string;
30
+ ranges: SourceRange[];
31
+ }
23
32
  interface PreprocessOptions {
24
33
  filePath?: string;
25
34
  seen?: Set<string>;
26
35
  }
36
+ export declare function preprocessSourceWithMetadata(source: string, options?: PreprocessOptions): PreprocessedSource;
27
37
  export declare function preprocessSource(source: string, options?: PreprocessOptions): string;
28
38
  export declare function compile(source: string, options?: CompileOptions): CompileResult;
29
39
  export declare function formatCompileError(result: CompileResult): string;
package/dist/compile.js CHANGED
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.preprocessSourceWithMetadata = preprocessSourceWithMetadata;
41
42
  exports.preprocessSource = preprocessSource;
42
43
  exports.compile = compile;
43
44
  exports.formatCompileError = formatCompileError;
@@ -50,7 +51,17 @@ const passes_1 = require("./optimizer/passes");
50
51
  const mcfunction_1 = require("./codegen/mcfunction");
51
52
  const diagnostics_1 = require("./diagnostics");
52
53
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/;
53
- function preprocessSource(source, options = {}) {
54
+ function countLines(source) {
55
+ return source === '' ? 0 : source.split('\n').length;
56
+ }
57
+ function offsetRanges(ranges, lineOffset) {
58
+ return ranges.map(range => ({
59
+ startLine: range.startLine + lineOffset,
60
+ endLine: range.endLine + lineOffset,
61
+ filePath: range.filePath,
62
+ }));
63
+ }
64
+ function preprocessSourceWithMetadata(source, options = {}) {
54
65
  const { filePath } = options;
55
66
  const seen = options.seen ?? new Set();
56
67
  if (filePath) {
@@ -78,7 +89,7 @@ function preprocessSource(source, options = {}) {
78
89
  catch {
79
90
  throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
80
91
  }
81
- imports.push(preprocessSource(importedSource, { filePath: importPath, seen }));
92
+ imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }));
82
93
  }
83
94
  continue;
84
95
  }
@@ -89,7 +100,26 @@ function preprocessSource(source, options = {}) {
89
100
  parsingHeader = false;
90
101
  bodyLines.push(line);
91
102
  }
92
- return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n');
103
+ const body = bodyLines.join('\n');
104
+ const parts = [...imports.map(entry => entry.source), body].filter(Boolean);
105
+ const combined = parts.join('\n');
106
+ const ranges = [];
107
+ let lineOffset = 0;
108
+ for (const entry of imports) {
109
+ ranges.push(...offsetRanges(entry.ranges, lineOffset));
110
+ lineOffset += countLines(entry.source);
111
+ }
112
+ if (filePath && body) {
113
+ ranges.push({
114
+ startLine: lineOffset + 1,
115
+ endLine: lineOffset + countLines(body),
116
+ filePath: path.resolve(filePath),
117
+ });
118
+ }
119
+ return { source: combined, ranges };
120
+ }
121
+ function preprocessSource(source, options = {}) {
122
+ return preprocessSourceWithMetadata(source, options).source;
93
123
  }
94
124
  // ---------------------------------------------------------------------------
95
125
  // Main Compile Function
@@ -98,14 +128,15 @@ function compile(source, options = {}) {
98
128
  const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options;
99
129
  let sourceLines = source.split('\n');
100
130
  try {
101
- const preprocessedSource = preprocessSource(source, { filePath });
131
+ const preprocessed = preprocessSourceWithMetadata(source, { filePath });
132
+ const preprocessedSource = preprocessed.source;
102
133
  sourceLines = preprocessedSource.split('\n');
103
134
  // Lexing
104
135
  const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
105
136
  // Parsing
106
137
  const ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
107
138
  // Lowering
108
- const ir = new lowering_1.Lowering(namespace).lower(ast);
139
+ const ir = new lowering_1.Lowering(namespace, preprocessed.ranges).lower(ast);
109
140
  // Optimization
110
141
  const optimized = shouldOptimize
111
142
  ? { ...ir, functions: ir.functions.map(fn => (0, passes_1.optimize)(fn)) }
@@ -0,0 +1,35 @@
1
+ import type { TypeNode } from '../ast/types';
2
+ export declare const EVENT_TYPES: {
3
+ readonly PlayerDeath: {
4
+ readonly tag: "rs.just_died";
5
+ readonly params: readonly ["player: Player"];
6
+ readonly detection: "scoreboard";
7
+ };
8
+ readonly PlayerJoin: {
9
+ readonly tag: "rs.just_joined";
10
+ readonly params: readonly ["player: Player"];
11
+ readonly detection: "tag";
12
+ };
13
+ readonly BlockBreak: {
14
+ readonly tag: "rs.just_broke_block";
15
+ readonly params: readonly ["player: Player", "block: string"];
16
+ readonly detection: "advancement";
17
+ };
18
+ readonly EntityKill: {
19
+ readonly tag: "rs.just_killed";
20
+ readonly params: readonly ["player: Player"];
21
+ readonly detection: "scoreboard";
22
+ };
23
+ readonly ItemUse: {
24
+ readonly tag: "rs.just_used_item";
25
+ readonly params: readonly ["player: Player"];
26
+ readonly detection: "scoreboard";
27
+ };
28
+ };
29
+ export type EventTypeName = keyof typeof EVENT_TYPES;
30
+ export interface EventParamSpec {
31
+ name: string;
32
+ type: TypeNode;
33
+ }
34
+ export declare function isEventTypeName(value: string): value is EventTypeName;
35
+ export declare function getEventParamSpecs(eventType: EventTypeName): EventParamSpec[];