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
@@ -25,6 +25,7 @@ describe('Parser', () => {
25
25
  const program = parse('')
26
26
  expect(program.namespace).toBe('test')
27
27
  expect(program.declarations).toEqual([])
28
+ expect(program.implBlocks).toEqual([])
28
29
  expect(program.enums).toEqual([])
29
30
  expect(program.consts).toEqual([])
30
31
  })
@@ -102,6 +103,13 @@ describe('Parser', () => {
102
103
  { name: 'on_death' },
103
104
  ])
104
105
  })
106
+
107
+ it('parses @on event decorators', () => {
108
+ const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}')
109
+ expect(program.declarations[0].decorators).toEqual([
110
+ { name: 'on', args: { eventType: 'PlayerDeath' } },
111
+ ])
112
+ })
105
113
  })
106
114
 
107
115
  describe('types', () => {
@@ -151,6 +159,52 @@ describe('Parser', () => {
151
159
  },
152
160
  ])
153
161
  })
162
+
163
+ it('parses impl blocks', () => {
164
+ const program = parse(`
165
+ struct Timer { duration: int }
166
+
167
+ impl Timer {
168
+ fn new(duration: int): Timer {
169
+ return { duration: duration };
170
+ }
171
+
172
+ fn start(self) {}
173
+ }
174
+ `)
175
+ expect(program.implBlocks).toHaveLength(1)
176
+ expect(program.implBlocks[0].typeName).toBe('Timer')
177
+ expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start'])
178
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
179
+ name: 'self',
180
+ type: { kind: 'struct', name: 'Timer' },
181
+ default: undefined,
182
+ })
183
+ })
184
+
185
+ it('parses impl blocks with static and instance methods', () => {
186
+ const program = parse(`
187
+ struct Point { x: int, y: int }
188
+
189
+ impl Point {
190
+ fn new(x: int, y: int) -> Point {
191
+ return { x: x, y: y };
192
+ }
193
+
194
+ fn distance(self) -> int {
195
+ return self.x + self.y;
196
+ }
197
+ }
198
+ `)
199
+ expect(program.implBlocks).toHaveLength(1)
200
+ expect(program.implBlocks[0].typeName).toBe('Point')
201
+ expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y'])
202
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
203
+ name: 'self',
204
+ type: { kind: 'struct', name: 'Point' },
205
+ default: undefined,
206
+ })
207
+ })
154
208
  })
155
209
 
156
210
  describe('statements', () => {
@@ -197,6 +251,28 @@ describe('Parser', () => {
197
251
  expect((stmt as any).else_).toHaveLength(1)
198
252
  })
199
253
 
254
+ it('parses entity is-checks in if conditions', () => {
255
+ const stmt = parseStmt('if (e is Player) { kill(@s); }')
256
+ expect(stmt.kind).toBe('if')
257
+ expect((stmt as any).cond).toEqual({
258
+ kind: 'is_check',
259
+ expr: { kind: 'ident', name: 'e' },
260
+ entityType: 'Player',
261
+ })
262
+ })
263
+
264
+ it('parses entity is-checks inside foreach bodies', () => {
265
+ const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }')
266
+ expect(stmt.kind).toBe('foreach')
267
+ const innerIf = (stmt as any).body[0]
268
+ expect(innerIf.kind).toBe('if')
269
+ expect(innerIf.cond).toEqual({
270
+ kind: 'is_check',
271
+ expr: { kind: 'ident', name: 'e' },
272
+ entityType: 'Zombie',
273
+ })
274
+ })
275
+
200
276
  it('parses while statement', () => {
201
277
  const stmt = parseStmt('while (i > 0) { i = i - 1; }')
202
278
  expect(stmt.kind).toBe('while')
@@ -459,11 +535,11 @@ describe('Parser', () => {
459
535
  expect(expr).toEqual({ kind: 'ident', name: 'foo' })
460
536
  })
461
537
 
462
- it('parses function call', () => {
463
- const expr = parseExpr('foo(1, 2)')
464
- expect(expr).toEqual({
465
- kind: 'call',
466
- fn: 'foo',
538
+ it('parses function call', () => {
539
+ const expr = parseExpr('foo(1, 2)')
540
+ expect(expr).toEqual({
541
+ kind: 'call',
542
+ fn: 'foo',
467
543
  args: [
468
544
  { kind: 'int_lit', value: 1 },
469
545
  { kind: 'int_lit', value: 2 },
@@ -486,6 +562,16 @@ describe('Parser', () => {
486
562
  })
487
563
  })
488
564
 
565
+ it('parses static method calls', () => {
566
+ const expr = parseExpr('Timer::new(100)')
567
+ expect(expr).toEqual({
568
+ kind: 'static_call',
569
+ type: 'Timer',
570
+ method: 'new',
571
+ args: [{ kind: 'int_lit', value: 100 }],
572
+ })
573
+ })
574
+
489
575
  describe('binary operators', () => {
490
576
  it('parses arithmetic', () => {
491
577
  const expr = parseExpr('1 + 2')
@@ -57,7 +57,7 @@ fn compute() {
57
57
  runtime.load()
58
58
  runtime.execFunction('compute')
59
59
 
60
- expect(runtime.getScore('math', 'result')).toBe(11)
60
+ expect(runtime.getScore('math', 'runtime.result')).toBe(11)
61
61
  })
62
62
 
63
63
  it('captures say, announce, actionbar, and title output in the chat log', () => {
@@ -135,8 +135,8 @@ fn arrays() {
135
135
  runtime.load()
136
136
  runtime.execFunction('arrays')
137
137
 
138
- expect(runtime.getScore('arrays', 'len')).toBe(1)
139
- expect(runtime.getScore('arrays', 'last')).toBe(9)
138
+ expect(runtime.getScore('arrays', 'runtime.len')).toBe(1)
139
+ expect(runtime.getScore('arrays', 'runtime.last')).toBe(9)
140
140
  expect(runtime.getStorage('rs:heap.arr')).toEqual([4])
141
141
  })
142
142
 
@@ -177,7 +177,7 @@ fn pulse() {
177
177
  runtime.load()
178
178
  runtime.ticks(10)
179
179
 
180
- expect(runtime.getScore('pulse', 'count')).toBe(2)
180
+ expect(runtime.getScore('pulse', 'runtime.count')).toBe(2)
181
181
  })
182
182
 
183
183
  it('executes only the matching match arm', () => {
@@ -234,7 +234,7 @@ fn test() {
234
234
  runtime.load()
235
235
  runtime.execFunction('test')
236
236
 
237
- expect(runtime.getScore('lambda', 'direct')).toBe(10)
237
+ expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10)
238
238
  })
239
239
 
240
240
  it('executes lambdas passed as callback arguments', () => {
@@ -252,7 +252,7 @@ fn test() {
252
252
  runtime.load()
253
253
  runtime.execFunction('test')
254
254
 
255
- expect(runtime.getScore('lambda', 'callback')).toBe(15)
255
+ expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15)
256
256
  })
257
257
 
258
258
  it('executes block-body lambdas', () => {
@@ -270,7 +270,7 @@ fn test() {
270
270
  runtime.load()
271
271
  runtime.execFunction('test')
272
272
 
273
- expect(runtime.getScore('lambda', 'block')).toBe(11)
273
+ expect(runtime.getScore('lambda', 'runtime.block')).toBe(11)
274
274
  })
275
275
 
276
276
  it('executes immediately-invoked expression-body lambdas', () => {
@@ -284,6 +284,6 @@ fn test() {
284
284
  runtime.load()
285
285
  runtime.execFunction('test')
286
286
 
287
- expect(runtime.getScore('lambda', 'iife')).toBe(10)
287
+ expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10)
288
288
  })
289
289
  })
@@ -211,6 +211,147 @@ fn test() {
211
211
  `)
212
212
  expect(errors).toHaveLength(0)
213
213
  })
214
+
215
+ it('type checks timer builtins with void callbacks and interval IDs', () => {
216
+ const errors = typeCheck(`
217
+ fn test() {
218
+ setTimeout(100, () => {
219
+ say("later");
220
+ });
221
+ let intervalId: int = setInterval(20, () => {
222
+ say("tick");
223
+ });
224
+ clearInterval(intervalId);
225
+ }
226
+ `)
227
+ expect(errors).toHaveLength(0)
228
+ })
229
+
230
+ it('rejects timer callbacks with the wrong return type', () => {
231
+ const errors = typeCheck(`
232
+ fn test() {
233
+ setTimeout(100, () => 1);
234
+ }
235
+ `)
236
+ expect(errors.length).toBeGreaterThan(0)
237
+ expect(errors[0].message).toContain('Return type mismatch: expected void, got int')
238
+ })
239
+
240
+ it('allows impl instance methods with inferred self type', () => {
241
+ const errors = typeCheck(`
242
+ struct Timer { duration: int }
243
+
244
+ impl Timer {
245
+ fn elapsed(self) -> int {
246
+ return self.duration;
247
+ }
248
+ }
249
+
250
+ fn test() {
251
+ let timer: Timer = { duration: 10 };
252
+ let value: int = timer.elapsed();
253
+ }
254
+ `)
255
+ expect(errors).toHaveLength(0)
256
+ })
257
+
258
+ it('records then-branch entity narrowing for is-checks', () => {
259
+ const source = `
260
+ fn test() {
261
+ foreach (e in @e) {
262
+ if (e is Player) {
263
+ kill(e);
264
+ }
265
+ }
266
+ }
267
+ `
268
+ const tokens = new Lexer(source).tokenize()
269
+ const ast = new Parser(tokens).parse('test')
270
+ const checker = new TypeChecker(source) as any
271
+ checker.check(ast)
272
+
273
+ const foreachStmt = ast.declarations[0].body[0] as any
274
+ const ifStmt = foreachStmt.body[0]
275
+ expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
276
+ name: 'e',
277
+ type: { kind: 'entity', entityType: 'Player' },
278
+ mutable: false,
279
+ })
280
+ })
281
+
282
+ it('allows static impl method calls', () => {
283
+ const errors = typeCheck(`
284
+ struct Timer { duration: int }
285
+
286
+ impl Timer {
287
+ fn new(duration: int) -> Timer {
288
+ return { duration: duration };
289
+ }
290
+ }
291
+
292
+ fn test() {
293
+ let timer: Timer = Timer::new(10);
294
+ }
295
+ `)
296
+ expect(errors).toHaveLength(0)
297
+ })
298
+
299
+ it('rejects using is-checks on non-entity values', () => {
300
+ const errors = typeCheck(`
301
+ fn test() {
302
+ let x: int = 1;
303
+ if (x is Player) {
304
+ say("nope");
305
+ }
306
+ }
307
+ `)
308
+ expect(errors.length).toBeGreaterThan(0)
309
+ expect(errors[0].message).toContain("'is' checks require an entity expression, got int")
310
+ })
311
+
312
+ it('rejects calling instance impl methods as static methods', () => {
313
+ const errors = typeCheck(`
314
+ struct Point { x: int, y: int }
315
+
316
+ impl Point {
317
+ fn distance(self) -> int {
318
+ return self.x + self.y;
319
+ }
320
+ }
321
+
322
+ fn test() {
323
+ let total: int = Point::distance();
324
+ }
325
+ `)
326
+ expect(errors.length).toBeGreaterThan(0)
327
+ expect(errors[0].message).toContain("Method 'Point::distance' is an instance method")
328
+ })
329
+ })
330
+
331
+ describe('entity is-check narrowing', () => {
332
+ it('allows entity type checks on foreach bindings', () => {
333
+ const errors = typeCheck(`
334
+ fn test() {
335
+ foreach (e in @e) {
336
+ if (e is Player) {
337
+ kill(@s);
338
+ }
339
+ }
340
+ }
341
+ `)
342
+ expect(errors).toHaveLength(0)
343
+ })
344
+
345
+ it('rejects is-checks on non-entity expressions', () => {
346
+ const errors = typeCheck(`
347
+ fn test() {
348
+ let x: int = 1;
349
+ if (x is Player) {}
350
+ }
351
+ `)
352
+ expect(errors.length).toBeGreaterThan(0)
353
+ expect(errors[0].message).toContain("'is' checks require an entity expression")
354
+ })
214
355
  })
215
356
 
216
357
  describe('return type checking', () => {
@@ -392,4 +533,34 @@ fn broken() -> int {
392
533
  expect(errors.length).toBeGreaterThanOrEqual(3)
393
534
  })
394
535
  })
536
+
537
+ describe('event handlers', () => {
538
+ it('accepts matching @on event signatures', () => {
539
+ const errors = typeCheck(`
540
+ @on(PlayerDeath)
541
+ fn handle_death(player: Player) {
542
+ tp(player, @p);
543
+ }
544
+ `)
545
+ expect(errors).toHaveLength(0)
546
+ })
547
+
548
+ it('rejects unknown event types', () => {
549
+ const errors = typeCheck(`
550
+ @on(NotARealEvent)
551
+ fn handle(player: Player) {}
552
+ `)
553
+ expect(errors.length).toBeGreaterThan(0)
554
+ expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'")
555
+ })
556
+
557
+ it('rejects mismatched event signatures', () => {
558
+ const errors = typeCheck(`
559
+ @on(BlockBreak)
560
+ fn handle_break(player: Player) {}
561
+ `)
562
+ expect(errors.length).toBeGreaterThan(0)
563
+ expect(errors[0].message).toContain('must declare 2 parameter(s)')
564
+ })
565
+ })
395
566
  })
package/src/ast/types.ts CHANGED
@@ -24,12 +24,26 @@ export interface Span {
24
24
 
25
25
  export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double'
26
26
 
27
+ // Entity type hierarchy
28
+ export type EntityTypeName =
29
+ | 'entity' // Base type
30
+ | 'Player' // @a, @p, @r
31
+ | 'Mob' // Base mob type
32
+ | 'HostileMob' // Hostile mobs
33
+ | 'PassiveMob' // Passive mobs
34
+ // Specific mob types (common ones)
35
+ | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman'
36
+ | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager'
37
+ | 'ArmorStand' | 'Item' | 'Arrow'
38
+
27
39
  export type TypeNode =
28
40
  | { kind: 'named'; name: PrimitiveType }
29
41
  | { kind: 'array'; elem: TypeNode }
30
42
  | { kind: 'struct'; name: string }
31
43
  | { kind: 'enum'; name: string }
32
44
  | { kind: 'function_type'; params: TypeNode[]; return: TypeNode }
45
+ | { kind: 'entity'; entityType: EntityTypeName } // Entity types
46
+ | { kind: 'selector' } // Selector type (multiple entities)
33
47
 
34
48
  export interface LambdaParam {
35
49
  name: string
@@ -124,6 +138,7 @@ export type Expr =
124
138
  | { kind: 'ident'; name: string; span?: Span }
125
139
  | { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
126
140
  | { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
141
+ | { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
127
142
  | { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
128
143
  | { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
129
144
  | { kind: 'call'; fn: string; args: Expr[]; span?: Span }
@@ -180,9 +195,10 @@ export type Block = Stmt[]
180
195
  // ---------------------------------------------------------------------------
181
196
 
182
197
  export interface Decorator {
183
- name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
198
+ name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
184
199
  args?: {
185
200
  rate?: number
201
+ eventType?: string
186
202
  trigger?: string
187
203
  advancement?: string
188
204
  item?: string
@@ -224,6 +240,13 @@ export interface StructDecl {
224
240
  span?: Span
225
241
  }
226
242
 
243
+ export interface ImplBlock {
244
+ kind: 'impl_block'
245
+ typeName: string
246
+ methods: FnDecl[]
247
+ span?: Span
248
+ }
249
+
227
250
  export interface EnumVariant {
228
251
  name: string
229
252
  value?: number
@@ -260,6 +283,7 @@ export interface Program {
260
283
  globals: GlobalDecl[]
261
284
  declarations: FnDecl[]
262
285
  structs: StructDecl[]
286
+ implBlocks: ImplBlock[]
263
287
  enums: EnumDecl[]
264
288
  consts: ConstDecl[]
265
289
  }
@@ -18,6 +18,7 @@
18
18
 
19
19
  import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
20
20
  import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
21
+ import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Utilities
@@ -270,6 +271,10 @@ export function generateDatapackWithStats(
270
271
  // Collect all trigger handlers
271
272
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
272
273
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
274
+ const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
275
+ !!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
276
+ )
277
+ const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
273
278
 
274
279
  // Collect all tick functions
275
280
  const tickFunctionNames: string[] = []
@@ -302,6 +307,19 @@ export function generateDatapackWithStats(
302
307
  loadLines.push(`scoreboard players enable @a ${triggerName}`)
303
308
  }
304
309
 
310
+ for (const eventType of eventTypes) {
311
+ const detection = EVENT_TYPES[eventType].detection
312
+ if (eventType === 'PlayerDeath') {
313
+ loadLines.push('scoreboard objectives add rs.deaths deathCount')
314
+ } else if (eventType === 'EntityKill') {
315
+ loadLines.push('scoreboard objectives add rs.kills totalKillCount')
316
+ } else if (eventType === 'ItemUse') {
317
+ loadLines.push('# ItemUse detection requires a project-specific objective/tag setup')
318
+ } else if (detection === 'tag' || detection === 'advancement') {
319
+ loadLines.push(`# ${eventType} detection expects tag ${EVENT_TYPES[eventType].tag} to be set externally`)
320
+ }
321
+ }
322
+
305
323
  // Generate trigger dispatch functions
306
324
  for (const triggerName of triggerNames) {
307
325
  const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName)
@@ -391,8 +409,20 @@ export function generateDatapackWithStats(
391
409
  }
392
410
  }
393
411
 
412
+ if (eventHandlers.length > 0) {
413
+ tickLines.push('# Event checks')
414
+ for (const eventType of eventTypes) {
415
+ const tag = EVENT_TYPES[eventType].tag
416
+ const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
417
+ for (const handler of handlers) {
418
+ tickLines.push(`execute as @a[tag=${tag}] run function ${ns}:${handler.name}`)
419
+ }
420
+ tickLines.push(`tag @a[tag=${tag}] remove ${tag}`)
421
+ }
422
+ }
423
+
394
424
  // Only generate __tick if there's something to run
395
- if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
425
+ if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
396
426
  files.push({
397
427
  path: `data/${ns}/function/__tick.mcfunction`,
398
428
  content: tickLines.join('\n'),
@@ -8,6 +8,7 @@ import { optimizeForStructure, optimizeForStructureWithStats } from '../../optim
8
8
  import { preprocessSource } from '../../compile'
9
9
  import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
10
10
  import type { DatapackFile } from '../mcfunction'
11
+ import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
11
12
 
12
13
  const DATA_VERSION = 3953
13
14
  const MAX_WIDTH = 16
@@ -87,6 +88,10 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
87
88
  const entries: CommandEntry[] = []
88
89
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
89
90
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
91
+ const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
92
+ !!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
93
+ )
94
+ const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
90
95
  const loadCommands = [
91
96
  `scoreboard objectives add ${OBJ} dummy`,
92
97
  ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
@@ -99,6 +104,14 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
99
104
  ).map(constSetup),
100
105
  ]
101
106
 
107
+ for (const eventType of eventTypes) {
108
+ if (eventType === 'PlayerDeath') {
109
+ loadCommands.push('scoreboard objectives add rs.deaths deathCount')
110
+ } else if (eventType === 'EntityKill') {
111
+ loadCommands.push('scoreboard objectives add rs.kills totalKillCount')
112
+ }
113
+ }
114
+
102
115
  // Call @load functions from __load
103
116
  for (const fn of module.functions) {
104
117
  if (fn.isLoadInit) {
@@ -146,6 +159,20 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
146
159
  })
147
160
  }
148
161
  }
162
+ if (eventHandlers.length > 0) {
163
+ for (const eventType of eventTypes) {
164
+ const tag = EVENT_TYPES[eventType].tag
165
+ const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
166
+ for (const handler of handlers) {
167
+ tickCommands.push({
168
+ cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
169
+ })
170
+ }
171
+ tickCommands.push({
172
+ cmd: `tag @a[tag=${tag}] remove ${tag}`,
173
+ })
174
+ }
175
+ }
149
176
  if (tickCommands.length > 0) {
150
177
  sections.push({
151
178
  name: '__tick',
package/src/compile.ts CHANGED
@@ -39,6 +39,17 @@ export interface CompileResult {
39
39
  error?: DiagnosticError
40
40
  }
41
41
 
42
+ export interface SourceRange {
43
+ startLine: number
44
+ endLine: number
45
+ filePath: string
46
+ }
47
+
48
+ export interface PreprocessedSource {
49
+ source: string
50
+ ranges: SourceRange[]
51
+ }
52
+
42
53
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
43
54
 
44
55
  interface PreprocessOptions {
@@ -46,7 +57,19 @@ interface PreprocessOptions {
46
57
  seen?: Set<string>
47
58
  }
48
59
 
49
- export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
60
+ function countLines(source: string): number {
61
+ return source === '' ? 0 : source.split('\n').length
62
+ }
63
+
64
+ function offsetRanges(ranges: SourceRange[], lineOffset: number): SourceRange[] {
65
+ return ranges.map(range => ({
66
+ startLine: range.startLine + lineOffset,
67
+ endLine: range.endLine + lineOffset,
68
+ filePath: range.filePath,
69
+ }))
70
+ }
71
+
72
+ export function preprocessSourceWithMetadata(source: string, options: PreprocessOptions = {}): PreprocessedSource {
50
73
  const { filePath } = options
51
74
  const seen = options.seen ?? new Set<string>()
52
75
 
@@ -55,7 +78,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
55
78
  }
56
79
 
57
80
  const lines = source.split('\n')
58
- const imports: string[] = []
81
+ const imports: PreprocessedSource[] = []
59
82
  const bodyLines: string[] = []
60
83
  let parsingHeader = true
61
84
 
@@ -90,7 +113,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
90
113
  )
91
114
  }
92
115
 
93
- imports.push(preprocessSource(importedSource, { filePath: importPath, seen }))
116
+ imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
94
117
  }
95
118
  continue
96
119
  }
@@ -104,7 +127,31 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
104
127
  bodyLines.push(line)
105
128
  }
106
129
 
107
- return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n')
130
+ const body = bodyLines.join('\n')
131
+ const parts = [...imports.map(entry => entry.source), body].filter(Boolean)
132
+ const combined = parts.join('\n')
133
+
134
+ const ranges: SourceRange[] = []
135
+ let lineOffset = 0
136
+
137
+ for (const entry of imports) {
138
+ ranges.push(...offsetRanges(entry.ranges, lineOffset))
139
+ lineOffset += countLines(entry.source)
140
+ }
141
+
142
+ if (filePath && body) {
143
+ ranges.push({
144
+ startLine: lineOffset + 1,
145
+ endLine: lineOffset + countLines(body),
146
+ filePath: path.resolve(filePath),
147
+ })
148
+ }
149
+
150
+ return { source: combined, ranges }
151
+ }
152
+
153
+ export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
154
+ return preprocessSourceWithMetadata(source, options).source
108
155
  }
109
156
 
110
157
  // ---------------------------------------------------------------------------
@@ -116,7 +163,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
116
163
  let sourceLines = source.split('\n')
117
164
 
118
165
  try {
119
- const preprocessedSource = preprocessSource(source, { filePath })
166
+ const preprocessed = preprocessSourceWithMetadata(source, { filePath })
167
+ const preprocessedSource = preprocessed.source
120
168
  sourceLines = preprocessedSource.split('\n')
121
169
 
122
170
  // Lexing
@@ -126,7 +174,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
126
174
  const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
127
175
 
128
176
  // Lowering
129
- const ir = new Lowering(namespace).lower(ast)
177
+ const ir = new Lowering(namespace, preprocessed.ranges).lower(ast)
130
178
 
131
179
  // Optimization
132
180
  const optimized: IRModule = shouldOptimize