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
@@ -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.rs'));
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
@@ -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: EntitySelector;
232
+ selector?: EntitySelector;
233
+ varName?: string;
234
+ filters?: SelectorFilter;
217
235
  } | {
218
236
  kind: 'unless_entity';
219
- selector: EntitySelector;
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 .rs file changes, recompile, and hot reload
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 .rs file changes...`);
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 .rs files in directory
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 .rs files found in ${dir}`);
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 .rs files recursively
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('.rs')) {
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('.rs')) {
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;
@@ -11,7 +11,7 @@
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
  */
@@ -12,7 +12,7 @@
12
12
  * Variable mapping:
13
13
  * scoreboard objective: "rs"
14
14
  * fake player: "$<varname>"
15
- * temporaries: "$t0", "$t1", ...
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} 0`);
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'),