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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "0.2.0",
9
+ "version": "1.0.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "0.5.0",
5
+ "version": "1.0.0",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "redscript"
37
37
  ],
38
38
  "extensions": [
39
- ".rs"
39
+ ".mcrs"
40
40
  ],
41
41
  "configuration": "./redscript-language-configuration.json"
42
42
  },
@@ -116,7 +116,14 @@
116
116
  }
117
117
  ]
118
118
  }
119
- }
119
+ },
120
+ "iconThemes": [
121
+ {
122
+ "id": "redscript-icons",
123
+ "label": "RedScript Icons",
124
+ "path": "./icons/redscript-icons.json"
125
+ }
126
+ ]
120
127
  },
121
128
  "scripts": {
122
129
  "compile": "node build.mjs",
@@ -766,17 +766,70 @@ function findFnDeclLine(document: vscode.TextDocument, name: string): number | n
766
766
  function findFnSignature(document: vscode.TextDocument, name: string): string | null {
767
767
  const text = document.getText()
768
768
  // Match: fn name(params) or fn name(params) -> Type
769
- const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))?`, 'm')
769
+ const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))?\\s*\\{`, 'm')
770
770
  const match = re.exec(text)
771
771
  if (!match) return null
772
772
  const params = match[1].trim()
773
- const returnType = match[2]
773
+ let returnType = match[2]
774
+
775
+ // If no explicit return type, try to infer from return statements
776
+ if (!returnType) {
777
+ returnType = inferReturnType(text, match.index + match[0].length)
778
+ }
779
+
774
780
  if (returnType) {
775
781
  return `fn ${name}(${params}) -> ${returnType}`
776
782
  }
777
783
  return `fn ${name}(${params})`
778
784
  }
779
785
 
786
+ /**
787
+ * Infer return type by looking at return statements in function body
788
+ */
789
+ function inferReturnType(text: string, bodyStart: number): string | null {
790
+ // Find the matching closing brace
791
+ let braceCount = 1
792
+ let pos = bodyStart
793
+ while (pos < text.length && braceCount > 0) {
794
+ if (text[pos] === '{') braceCount++
795
+ else if (text[pos] === '}') braceCount--
796
+ pos++
797
+ }
798
+ const body = text.slice(bodyStart, pos - 1)
799
+
800
+ // Look for return statements
801
+ const returnMatch = body.match(/\breturn\s+(.+?);/)
802
+ if (!returnMatch) return null
803
+
804
+ const returnExpr = returnMatch[1].trim()
805
+
806
+ // Infer type from expression
807
+ if (/^\d+$/.test(returnExpr)) return 'int'
808
+ if (/^\d+\.\d+$/.test(returnExpr)) return 'float'
809
+ if (/^\d+[bB]$/.test(returnExpr)) return 'byte'
810
+ if (/^\d+[sS]$/.test(returnExpr)) return 'short'
811
+ if (/^\d+[lL]$/.test(returnExpr)) return 'long'
812
+ if (/^\d+(\.\d+)?[dD]$/.test(returnExpr)) return 'double'
813
+ if (/^".*"$/.test(returnExpr)) return 'string'
814
+ if (/^(true|false)$/.test(returnExpr)) return 'bool'
815
+ if (/^@[aeprs]/.test(returnExpr)) return 'selector'
816
+ if (/^\{/.test(returnExpr)) return 'struct'
817
+ if (/^\[/.test(returnExpr)) return 'array'
818
+
819
+ // Check for known builtin return types
820
+ const callMatch = returnExpr.match(/^(\w+)\s*\(/)
821
+ if (callMatch) {
822
+ const fnName = callMatch[1]
823
+ // Common builtins that return int
824
+ if (['scoreboard_get', 'score', 'random', 'random_native', 'str_len', 'len', 'data_get', 'bossbar_get_value', 'set_contains'].includes(fnName)) {
825
+ return 'int'
826
+ }
827
+ if (fnName === 'set_new') return 'string'
828
+ }
829
+
830
+ return null
831
+ }
832
+
780
833
  function escapeRe(s: string): string {
781
834
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
782
835
  }
@@ -77,6 +77,38 @@ function isStructLiteralField(doc: vscode.TextDocument, position: vscode.Positio
77
77
  return null
78
78
  }
79
79
 
80
+ /**
81
+ * Check if cursor is on a member access field: expr.field
82
+ * Returns the struct type name if found, null otherwise
83
+ */
84
+ function isMemberAccessField(doc: vscode.TextDocument, position: vscode.Position, word: string): string | null {
85
+ const line = doc.lineAt(position.line).text
86
+ const wordStart = position.character
87
+
88
+ // Check if word is preceded by '.'
89
+ const beforeWord = line.slice(0, wordStart)
90
+ if (!beforeWord.endsWith('.')) return null
91
+
92
+ // Find the variable before the dot
93
+ const varMatch = beforeWord.match(/(\w+)\s*\.$/)
94
+ if (!varMatch) return null
95
+ const varName = varMatch[1]
96
+
97
+ // Find the variable's type declaration
98
+ const text = doc.getText()
99
+ // Look for: let varName: TypeName or fn param varName: TypeName
100
+ const typeRe = new RegExp(`\\b(?:let|const)\\s+${varName}\\s*:\\s*(\\w+)`, 'm')
101
+ const typeMatch = text.match(typeRe)
102
+ if (typeMatch) return typeMatch[1]
103
+
104
+ // Also check function parameters: fn xxx(varName: TypeName)
105
+ const paramRe = new RegExp(`\\((?:[^)]*,\\s*)?${varName}\\s*:\\s*(\\w+)`, 'm')
106
+ const paramMatch = text.match(paramRe)
107
+ if (paramMatch) return paramMatch[1]
108
+
109
+ return null
110
+ }
111
+
80
112
  function findAllOccurrences(doc: vscode.TextDocument, word: string): vscode.Location[] {
81
113
  const text = doc.getText()
82
114
  const re = new RegExp(`\\b${escapeRegex(word)}\\b`, 'g')
@@ -119,6 +151,16 @@ export function registerSymbolProviders(context: vscode.ExtensionContext): void
119
151
  }
120
152
  }
121
153
 
154
+ // Check if this is a member access: expr.field
155
+ const memberAccess = isMemberAccessField(doc, position, word)
156
+ if (memberAccess) {
157
+ const structFields = findStructFields(doc)
158
+ const field = structFields.find(f => f.structName === memberAccess && f.fieldName === word)
159
+ if (field) {
160
+ return new vscode.Location(doc.uri, field.fieldRange)
161
+ }
162
+ }
163
+
122
164
  const decls = findDeclarations(doc)
123
165
  const decl = decls.find(d => d.name === word)
124
166
  if (!decl) return null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -10,11 +10,11 @@ describe('CLI API', () => {
10
10
  describe('imports', () => {
11
11
  it('compiles a file with imported helpers', () => {
12
12
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-imports-'))
13
- const libPath = path.join(tempDir, 'lib.rs')
14
- const mainPath = path.join(tempDir, 'main.rs')
13
+ const libPath = path.join(tempDir, 'lib.mcrs')
14
+ const mainPath = path.join(tempDir, 'main.mcrs')
15
15
 
16
16
  fs.writeFileSync(libPath, 'fn double(x: int) -> int { return x + x; }\n')
17
- fs.writeFileSync(mainPath, 'import "./lib.rs"\n\nfn main() { let value: int = double(2); }\n')
17
+ fs.writeFileSync(mainPath, 'import "./lib.mcrs"\n\nfn main() { let value: int = double(2); }\n')
18
18
 
19
19
  const source = fs.readFileSync(mainPath, 'utf-8')
20
20
  const result = compile(source, { namespace: 'imports', filePath: mainPath })
@@ -26,13 +26,13 @@ describe('CLI API', () => {
26
26
 
27
27
  it('deduplicates circular imports', () => {
28
28
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-circular-'))
29
- const aPath = path.join(tempDir, 'a.rs')
30
- const bPath = path.join(tempDir, 'b.rs')
31
- const mainPath = path.join(tempDir, 'main.rs')
29
+ const aPath = path.join(tempDir, 'a.mcrs')
30
+ const bPath = path.join(tempDir, 'b.mcrs')
31
+ const mainPath = path.join(tempDir, 'main.mcrs')
32
32
 
33
- fs.writeFileSync(aPath, 'import "./b.rs"\n\nfn from_a() -> int { return 1; }\n')
34
- fs.writeFileSync(bPath, 'import "./a.rs"\n\nfn from_b() -> int { return from_a(); }\n')
35
- fs.writeFileSync(mainPath, 'import "./a.rs"\n\nfn main() { let value: int = from_b(); }\n')
33
+ fs.writeFileSync(aPath, 'import "./b.mcrs"\n\nfn from_a() -> int { return 1; }\n')
34
+ fs.writeFileSync(bPath, 'import "./a.mcrs"\n\nfn from_b() -> int { return from_a(); }\n')
35
+ fs.writeFileSync(mainPath, 'import "./a.mcrs"\n\nfn main() { let value: int = from_b(); }\n')
36
36
 
37
37
  const source = fs.readFileSync(mainPath, 'utf-8')
38
38
  const result = compile(source, { namespace: 'circular', filePath: mainPath })
@@ -40,6 +40,172 @@ describe('CLI API', () => {
40
40
  expect(result.ir.functions.filter(fn => fn.name === 'from_a')).toHaveLength(1)
41
41
  expect(result.ir.functions.filter(fn => fn.name === 'from_b')).toHaveLength(1)
42
42
  })
43
+
44
+ it('uses rs-prefixed scoreboard objectives for imported stdlib files', () => {
45
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-'))
46
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib')
47
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
48
+ const mainPath = path.join(tempDir, 'main.mcrs')
49
+
50
+ fs.mkdirSync(stdlibDir, { recursive: true })
51
+ fs.writeFileSync(stdlibPath, 'fn tick_timer() { scoreboard_set("#rs", "timer_ticks", 1); }\n')
52
+ fs.writeFileSync(mainPath, 'import "./src/stdlib/timer.mcrs"\n\nfn main() { tick_timer(); }\n')
53
+
54
+ const source = fs.readFileSync(mainPath, 'utf-8')
55
+ const result = compile(source, { namespace: 'mygame', filePath: mainPath })
56
+ const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'))
57
+
58
+ expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1')
59
+ })
60
+
61
+ it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
62
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-hash-'))
63
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib')
64
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
65
+ const mainPath = path.join(tempDir, 'main.mcrs')
66
+
67
+ fs.mkdirSync(stdlibDir, { recursive: true })
68
+ fs.writeFileSync(stdlibPath, [
69
+ 'fn timer_start(name: string, duration: int) {',
70
+ ' scoreboard_set("timer_ticks", #rs, duration);',
71
+ ' scoreboard_set("timer_active", #rs, 1);',
72
+ '}',
73
+ '',
74
+ ].join('\n'))
75
+ fs.writeFileSync(mainPath, [
76
+ 'import "./src/stdlib/timer.mcrs"',
77
+ '',
78
+ 'fn main() {',
79
+ ' timer_start("x", 100);',
80
+ ' timer_start("x", 100);',
81
+ '}',
82
+ '',
83
+ ].join('\n'))
84
+
85
+ const source = fs.readFileSync(mainPath, 'utf-8')
86
+ const result = compile(source, { namespace: 'mygame', filePath: mainPath })
87
+ const timerFns = result.files.filter(file => /timer_start__callsite_[0-9a-f]{4}\.mcfunction$/.test(file.path))
88
+
89
+ expect(timerFns).toHaveLength(2)
90
+
91
+ const objectives = timerFns
92
+ .flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]))
93
+
94
+ expect(new Set(objectives).size).toBe(2)
95
+ })
96
+
97
+ it('Timer::new creates timer', () => {
98
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-new-'))
99
+ const mainPath = path.join(tempDir, 'main.mcrs')
100
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
101
+
102
+ fs.writeFileSync(mainPath, [
103
+ `import "${timerPath}"`,
104
+ '',
105
+ 'fn main() {',
106
+ ' let timer: Timer = Timer::new(20);',
107
+ '}',
108
+ '',
109
+ ].join('\n'))
110
+
111
+ const source = fs.readFileSync(mainPath, 'utf-8')
112
+ const result = compile(source, { namespace: 'timernew', filePath: mainPath })
113
+
114
+ expect(result.typeErrors).toEqual([])
115
+ const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'))
116
+ expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0')
117
+ expect(newFn?.content).toContain('scoreboard players set timer_active rs 0')
118
+ })
119
+
120
+ it('Timer.start/pause/reset', () => {
121
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-state-'))
122
+ const mainPath = path.join(tempDir, 'main.mcrs')
123
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
124
+
125
+ fs.writeFileSync(mainPath, [
126
+ `import "${timerPath}"`,
127
+ '',
128
+ 'fn main() {',
129
+ ' let timer: Timer = Timer::new(20);',
130
+ ' timer.start();',
131
+ ' timer.pause();',
132
+ ' timer.reset();',
133
+ '}',
134
+ '',
135
+ ].join('\n'))
136
+
137
+ const source = fs.readFileSync(mainPath, 'utf-8')
138
+ const result = compile(source, { namespace: 'timerstate', filePath: mainPath })
139
+
140
+ expect(result.typeErrors).toEqual([])
141
+ const startFn = result.files.find(file => file.path.endsWith('/Timer_start.mcfunction'))
142
+ const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'))
143
+ const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'))
144
+
145
+ expect(startFn?.content).toContain('scoreboard players set timer_active rs 1')
146
+ expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0')
147
+ expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0')
148
+ })
149
+
150
+ it('Timer.done returns bool', () => {
151
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-done-'))
152
+ const mainPath = path.join(tempDir, 'main.mcrs')
153
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
154
+
155
+ fs.writeFileSync(mainPath, [
156
+ `import "${timerPath}"`,
157
+ '',
158
+ 'fn main() {',
159
+ ' let timer: Timer = Timer::new(20);',
160
+ ' let finished: bool = timer.done();',
161
+ ' if (finished) {',
162
+ ' say("done");',
163
+ ' }',
164
+ '}',
165
+ '',
166
+ ].join('\n'))
167
+
168
+ const source = fs.readFileSync(mainPath, 'utf-8')
169
+ const result = compile(source, { namespace: 'timerdone', filePath: mainPath })
170
+
171
+ expect(result.typeErrors).toEqual([])
172
+ const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'))
173
+ const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'))
174
+ expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs')
175
+ expect(doneFn?.content).toContain('return run scoreboard players get')
176
+ expect(mainFn?.content).toContain('execute if score $finished rs matches 1..')
177
+ })
178
+
179
+ it('Timer.tick increments', () => {
180
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-tick-'))
181
+ const mainPath = path.join(tempDir, 'main.mcrs')
182
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
183
+
184
+ fs.writeFileSync(mainPath, [
185
+ `import "${timerPath}"`,
186
+ '',
187
+ 'fn main() {',
188
+ ' let timer: Timer = Timer::new(20);',
189
+ ' timer.start();',
190
+ ' timer.tick();',
191
+ '}',
192
+ '',
193
+ ].join('\n'))
194
+
195
+ const source = fs.readFileSync(mainPath, 'utf-8')
196
+ const result = compile(source, { namespace: 'timertick', filePath: mainPath })
197
+
198
+ expect(result.typeErrors).toEqual([])
199
+ const tickOutput = result.files
200
+ .filter(file => file.path.includes('/Timer_tick'))
201
+ .map(file => file.content)
202
+ .join('\n')
203
+
204
+ expect(tickOutput).toContain('scoreboard players get timer_active rs')
205
+ expect(tickOutput).toContain('scoreboard players get timer_ticks rs')
206
+ expect(tickOutput).toContain(' += $const_1 rs')
207
+ expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_')
208
+ })
43
209
  })
44
210
 
45
211
  describe('compile()', () => {
@@ -91,7 +257,7 @@ fn build() {
91
257
  describe('--stats flag', () => {
92
258
  it('prints optimizer statistics', () => {
93
259
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stats-'))
94
- const inputPath = path.join(tempDir, 'input.rs')
260
+ const inputPath = path.join(tempDir, 'input.mcrs')
95
261
  const outputDir = path.join(tempDir, 'out')
96
262
 
97
263
  fs.writeFileSync(inputPath, 'fn build() { setblock((0, 64, 0), "minecraft:stone"); setblock((1, 64, 0), "minecraft:stone"); }')
@@ -11,7 +11,7 @@ describe('generateDatapack', () => {
11
11
  })
12
12
 
13
13
  it('generates __load.mcfunction with objective setup', () => {
14
- const mod: IRModule = { namespace: 'mypack', functions: [], globals: ['counter'] }
14
+ const mod: IRModule = { namespace: 'mypack', functions: [], globals: [{ name: 'counter', init: 0 }] }
15
15
  const files = generateDatapack(mod)
16
16
  const load = files.find(f => f.path.includes('__load.mcfunction'))
17
17
  expect(load?.content).toContain('scoreboard objectives add rs dummy')
@@ -125,4 +125,31 @@ describe('generateDatapack', () => {
125
125
  expect(json.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond')
126
126
  expect(json.rewards.function).toBe('mypack:on_mine_diamond')
127
127
  })
128
+
129
+ it('generates static event dispatcher in __tick', () => {
130
+ const mod: IRModule = {
131
+ namespace: 'mypack',
132
+ globals: [],
133
+ functions: [{
134
+ name: 'handle_death',
135
+ params: [],
136
+ locals: [],
137
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
138
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
139
+ }, {
140
+ name: 'handle_death_2',
141
+ params: [],
142
+ locals: [],
143
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
144
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
145
+ }],
146
+ }
147
+
148
+ const files = generateDatapack(mod)
149
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'))
150
+ expect(tickFn).toBeDefined()
151
+ expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death')
152
+ expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death_2')
153
+ expect(tickFn!.content).toContain('tag @a[tag=rs.just_died] remove rs.just_died')
154
+ })
128
155
  })
@@ -33,11 +33,11 @@ describe('DiagnosticError', () => {
33
33
  const error = new DiagnosticError(
34
34
  'TypeError',
35
35
  'Unknown function: foo',
36
- { file: 'test.rs', line: 1, col: 9 },
36
+ { file: 'test.mcrs', line: 1, col: 9 },
37
37
  source.split('\n')
38
38
  )
39
39
 
40
- expect(formatError(error, source)).toContain('Error in test.rs at line 1, col 9:')
40
+ expect(formatError(error, source)).toContain('Error in test.mcrs at line 1, col 9:')
41
41
  })
42
42
  })
43
43
 
@@ -66,11 +66,11 @@ describe('DiagnosticError', () => {
66
66
  const error = new DiagnosticError(
67
67
  'LexError',
68
68
  'Unexpected character',
69
- { file: 'test.rs', line: 1, col: 1 },
69
+ { file: 'test.mcrs', line: 1, col: 1 },
70
70
  ['@@@']
71
71
  )
72
72
  const formatted = error.format()
73
- expect(formatted).toContain('test.rs:')
73
+ expect(formatted).toContain('test.mcrs:')
74
74
  expect(formatted).toContain('[LexError]')
75
75
  })
76
76
 
@@ -153,7 +153,7 @@ describe('compile function', () => {
153
153
  })
154
154
 
155
155
  it('includes file path in error', () => {
156
- const result = compile('fn main() { }', { filePath: 'test.rs' })
156
+ const result = compile('fn main() { }', { filePath: 'test.mcrs' })
157
157
  // This is valid, but test that filePath is passed through
158
158
  expect(result.success).toBe(true)
159
159
  })