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.
Files changed (136) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +112 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +148 -10
  9. package/dist/__tests__/codegen.test.js +26 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +336 -17
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lexer.test.js +12 -2
  15. package/dist/__tests__/lowering.test.js +200 -12
  16. package/dist/__tests__/mc-integration.test.js +370 -31
  17. package/dist/__tests__/mc-syntax.test.js +3 -3
  18. package/dist/__tests__/nbt.test.js +2 -2
  19. package/dist/__tests__/optimizer-advanced.test.js +5 -5
  20. package/dist/__tests__/parser.test.js +80 -0
  21. package/dist/__tests__/runtime.test.js +9 -9
  22. package/dist/__tests__/typechecker.test.js +158 -0
  23. package/dist/ast/types.d.ts +40 -3
  24. package/dist/cli.js +25 -7
  25. package/dist/codegen/mcfunction/index.d.ts +1 -1
  26. package/dist/codegen/mcfunction/index.js +38 -3
  27. package/dist/codegen/structure/index.js +32 -1
  28. package/dist/compile.d.ts +10 -0
  29. package/dist/compile.js +36 -5
  30. package/dist/events/types.d.ts +35 -0
  31. package/dist/events/types.js +59 -0
  32. package/dist/formatter/index.d.ts +1 -0
  33. package/dist/formatter/index.js +26 -0
  34. package/dist/index.js +3 -2
  35. package/dist/ir/builder.d.ts +2 -1
  36. package/dist/ir/types.d.ts +11 -2
  37. package/dist/ir/types.js +1 -1
  38. package/dist/lexer/index.d.ts +1 -1
  39. package/dist/lexer/index.js +2 -0
  40. package/dist/lowering/index.d.ts +34 -1
  41. package/dist/lowering/index.js +622 -23
  42. package/dist/mc-test/runner.d.ts +2 -2
  43. package/dist/mc-test/runner.js +3 -3
  44. package/dist/mc-test/setup.js +2 -2
  45. package/dist/parser/index.d.ts +4 -0
  46. package/dist/parser/index.js +153 -16
  47. package/dist/typechecker/index.d.ts +17 -0
  48. package/dist/typechecker/index.js +343 -17
  49. package/docs/COMPILATION_STATS.md +24 -24
  50. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  51. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  52. package/docs/STRUCTURE_TARGET.md +1 -1
  53. package/editors/vscode/.vscodeignore +1 -0
  54. package/editors/vscode/CHANGELOG.md +9 -0
  55. package/editors/vscode/icons/mcrs.svg +7 -0
  56. package/editors/vscode/icons/redscript-icons.json +10 -0
  57. package/editors/vscode/out/extension.js +1295 -80
  58. package/editors/vscode/package-lock.json +2 -2
  59. package/editors/vscode/package.json +10 -3
  60. package/editors/vscode/src/hover.ts +55 -2
  61. package/editors/vscode/src/symbols.ts +42 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/cli.test.ts +176 -10
  64. package/src/__tests__/codegen.test.ts +28 -1
  65. package/src/__tests__/diagnostics.test.ts +5 -5
  66. package/src/__tests__/e2e.test.ts +335 -17
  67. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  68. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  69. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  70. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  71. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  72. package/src/__tests__/lexer.test.ts +14 -2
  73. package/src/__tests__/lowering.test.ts +226 -12
  74. package/src/__tests__/mc-integration.test.ts +421 -31
  75. package/src/__tests__/mc-syntax.test.ts +3 -3
  76. package/src/__tests__/nbt.test.ts +2 -2
  77. package/src/__tests__/optimizer-advanced.test.ts +5 -5
  78. package/src/__tests__/parser.test.ts +91 -5
  79. package/src/__tests__/runtime.test.ts +9 -9
  80. package/src/__tests__/typechecker.test.ts +171 -0
  81. package/src/ast/types.ts +44 -3
  82. package/src/cli.ts +10 -10
  83. package/src/codegen/mcfunction/index.ts +40 -3
  84. package/src/codegen/structure/index.ts +35 -1
  85. package/src/compile.ts +54 -6
  86. package/src/events/types.ts +69 -0
  87. package/src/examples/capture_the_flag.mcrs +208 -0
  88. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  89. package/src/examples/hunger_games.mcrs +301 -0
  90. package/src/examples/new_features_demo.mcrs +193 -0
  91. package/src/examples/parkour_race.mcrs +233 -0
  92. package/src/examples/rpg.mcrs +13 -0
  93. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  94. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  95. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  96. package/src/examples/zombie_survival.mcrs +314 -0
  97. package/src/index.ts +4 -3
  98. package/src/ir/builder.ts +3 -1
  99. package/src/ir/types.ts +12 -2
  100. package/src/lexer/index.ts +3 -1
  101. package/src/lowering/index.ts +684 -24
  102. package/src/mc-test/runner.ts +3 -3
  103. package/src/mc-test/setup.ts +2 -2
  104. package/src/parser/index.ts +170 -19
  105. package/src/stdlib/README.md +178 -140
  106. package/src/stdlib/bossbar.mcrs +68 -0
  107. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  108. package/src/stdlib/effects.mcrs +64 -0
  109. package/src/stdlib/interactions.mcrs +195 -0
  110. package/src/stdlib/inventory.mcrs +38 -0
  111. package/src/stdlib/mobs.mcrs +99 -0
  112. package/src/stdlib/particles.mcrs +52 -0
  113. package/src/stdlib/sets.mcrs +20 -0
  114. package/src/stdlib/spawn.mcrs +41 -0
  115. package/src/stdlib/tags.mcrs +951 -0
  116. package/src/stdlib/teams.mcrs +68 -0
  117. package/src/stdlib/timer.mcrs +72 -0
  118. package/src/stdlib/world.mcrs +92 -0
  119. package/src/typechecker/index.ts +404 -18
  120. package/src/examples/rpg.rs +0 -13
  121. package/src/stdlib/mobs.rs +0 -99
  122. package/src/stdlib/timer.rs +0 -51
  123. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  124. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  125. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  126. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  127. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  128. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  129. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  130. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  131. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  132. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  133. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  134. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  135. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  136. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -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')
@@ -29,7 +29,7 @@ function loadExample(name: string): string {
29
29
 
30
30
  describe('MCRuntime behavioral integration', () => {
31
31
  it('runs the counter example and increments the scoreboard across ticks', () => {
32
- const runtime = loadCompiledProgram(loadExample('counter.rs'))
32
+ const runtime = loadCompiledProgram(loadExample('counter.mcrs'))
33
33
 
34
34
  runtime.load()
35
35
  runtime.ticks(5)
@@ -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
@@ -68,6 +82,13 @@ export interface SelectorFilter {
68
82
  sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary'
69
83
  nbt?: string
70
84
  gamemode?: string
85
+ // Position filters
86
+ x?: RangeExpr
87
+ y?: RangeExpr
88
+ z?: RangeExpr
89
+ // Rotation filters
90
+ x_rotation?: RangeExpr
91
+ y_rotation?: RangeExpr
71
92
  }
72
93
 
73
94
  export interface EntitySelector {
@@ -117,6 +138,7 @@ export type Expr =
117
138
  | { kind: 'ident'; name: string; span?: Span }
118
139
  | { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
119
140
  | { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
141
+ | { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
120
142
  | { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
121
143
  | { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
122
144
  | { kind: 'call'; fn: string; args: Expr[]; span?: Span }
@@ -146,8 +168,8 @@ export type LiteralExpr =
146
168
  export type ExecuteSubcommand =
147
169
  | { kind: 'as'; selector: EntitySelector }
148
170
  | { kind: 'at'; selector: EntitySelector }
149
- | { kind: 'if_entity'; selector: EntitySelector }
150
- | { kind: 'unless_entity'; selector: EntitySelector }
171
+ | { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
172
+ | { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
151
173
  | { kind: 'in'; dimension: string }
152
174
 
153
175
  export type Stmt =
@@ -173,9 +195,10 @@ export type Block = Stmt[]
173
195
  // ---------------------------------------------------------------------------
174
196
 
175
197
  export interface Decorator {
176
- name: 'tick' | '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'
177
199
  args?: {
178
200
  rate?: number
201
+ eventType?: string
179
202
  trigger?: string
180
203
  advancement?: string
181
204
  item?: string
@@ -217,6 +240,13 @@ export interface StructDecl {
217
240
  span?: Span
218
241
  }
219
242
 
243
+ export interface ImplBlock {
244
+ kind: 'impl_block'
245
+ typeName: string
246
+ methods: FnDecl[]
247
+ span?: Span
248
+ }
249
+
220
250
  export interface EnumVariant {
221
251
  name: string
222
252
  value?: number
@@ -235,14 +265,25 @@ export interface ConstDecl {
235
265
  span?: Span
236
266
  }
237
267
 
268
+ export interface GlobalDecl {
269
+ kind: 'global'
270
+ name: string
271
+ type: TypeNode
272
+ init: Expr
273
+ mutable: boolean // let = true, const = false
274
+ span?: Span
275
+ }
276
+
238
277
  // ---------------------------------------------------------------------------
239
278
  // Program (Top-Level)
240
279
  // ---------------------------------------------------------------------------
241
280
 
242
281
  export interface Program {
243
282
  namespace: string // Inferred from filename or `namespace mypack;`
283
+ globals: GlobalDecl[]
244
284
  declarations: FnDecl[]
245
285
  structs: StructDecl[]
286
+ implBlocks: ImplBlock[]
246
287
  enums: EnumDecl[]
247
288
  consts: ConstDecl[]
248
289
  }
package/src/cli.ts CHANGED
@@ -29,13 +29,13 @@ Usage:
29
29
  redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
30
30
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
31
  redscript check <file>
32
- redscript fmt <file.rs> [file2.rs ...]
32
+ redscript fmt <file.mcrs> [file2.mcrs ...]
33
33
  redscript repl
34
34
  redscript version
35
35
 
36
36
  Commands:
37
37
  compile Compile a RedScript file to a Minecraft datapack
38
- watch Watch a directory for .rs file changes, recompile, and hot reload
38
+ watch Watch a directory for .mcrs file changes, recompile, and hot reload
39
39
  check Check a RedScript file for errors without generating output
40
40
  fmt Auto-format RedScript source files
41
41
  repl Start an interactive RedScript REPL
@@ -268,7 +268,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
268
268
  process.exit(1)
269
269
  }
270
270
 
271
- console.log(`👁 Watching ${dir} for .rs file changes...`)
271
+ console.log(`👁 Watching ${dir} for .mcrs file changes...`)
272
272
  console.log(` Output: ${output}`)
273
273
  if (hotReloadUrl) console.log(` Hot reload: ${hotReloadUrl}`)
274
274
  console.log(` Press Ctrl+C to stop\n`)
@@ -276,11 +276,11 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
276
276
  // Debounce timer
277
277
  let debounceTimer: NodeJS.Timeout | null = null
278
278
 
279
- // Compile all .rs files in directory
279
+ // Compile all .mcrs files in directory
280
280
  async function compileAll(): Promise<void> {
281
281
  const files = findRsFiles(dir)
282
282
  if (files.length === 0) {
283
- console.log(`⚠ No .rs files found in ${dir}`)
283
+ console.log(`⚠ No .mcrs files found in ${dir}`)
284
284
  return
285
285
  }
286
286
 
@@ -319,7 +319,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
319
319
  }
320
320
  }
321
321
 
322
- // Find all .rs files recursively
322
+ // Find all .mcrs files recursively
323
323
  function findRsFiles(directory: string): string[] {
324
324
  const results: string[] = []
325
325
  const entries = fs.readdirSync(directory, { withFileTypes: true })
@@ -328,7 +328,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
328
328
  const fullPath = path.join(directory, entry.name)
329
329
  if (entry.isDirectory()) {
330
330
  results.push(...findRsFiles(fullPath))
331
- } else if (entry.isFile() && entry.name.endsWith('.rs')) {
331
+ } else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
332
332
  results.push(fullPath)
333
333
  }
334
334
  }
@@ -341,7 +341,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
341
341
 
342
342
  // Watch for changes
343
343
  fs.watch(dir, { recursive: true }, (eventType, filename) => {
344
- if (filename && filename.endsWith('.rs')) {
344
+ if (filename && filename.endsWith('.mcrs')) {
345
345
  // Debounce rapid changes
346
346
  if (debounceTimer) {
347
347
  clearTimeout(debounceTimer)
@@ -412,9 +412,9 @@ async function main(): Promise<void> {
412
412
 
413
413
  case 'fmt':
414
414
  case 'format': {
415
- const files = args.filter(a => a.endsWith('.rs'))
415
+ const files = args.filter(a => a.endsWith('.mcrs'))
416
416
  if (files.length === 0) {
417
- console.error('Usage: redscript fmt <file.rs> [file2.rs ...]')
417
+ console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]')
418
418
  process.exit(1)
419
419
  }
420
420
  const { format } = require('./formatter')
@@ -11,13 +11,14 @@
11
11
  * Variable mapping:
12
12
  * scoreboard objective: "rs"
13
13
  * fake player: "$<varname>"
14
- * temporaries: "$t0", "$t1", ...
14
+ * temporaries: "$_0", "$_1", ...
15
15
  * return value: "$ret"
16
16
  * parameters: "$p0", "$p1", ...
17
17
  */
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[] = []
@@ -293,7 +298,7 @@ export function generateDatapackWithStats(
293
298
  `scoreboard objectives add ${OBJ} dummy`,
294
299
  ]
295
300
  for (const g of module.globals) {
296
- loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ} 0`)
301
+ loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
297
302
  }
298
303
 
299
304
  // Add trigger objectives
@@ -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)
@@ -356,6 +374,13 @@ export function generateDatapackWithStats(
356
374
  }
357
375
  }
358
376
 
377
+ // Call @load functions from __load
378
+ for (const fn of module.functions) {
379
+ if (fn.isLoadInit) {
380
+ loadLines.push(`function ${ns}:${fn.name}`)
381
+ }
382
+ }
383
+
359
384
  // Write __load.mcfunction
360
385
  files.push({
361
386
  path: `data/${ns}/function/__load.mcfunction`,
@@ -384,8 +409,20 @@ export function generateDatapackWithStats(
384
409
  }
385
410
  }
386
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
+
387
424
  // Only generate __tick if there's something to run
388
- if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
425
+ if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
389
426
  files.push({
390
427
  path: `data/${ns}/function/__tick.mcfunction`,
391
428
  content: tickLines.join('\n'),