redscript-mc 1.0.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 (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
package/src/cli.ts ADDED
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * RedScript CLI
4
+ *
5
+ * Usage:
6
+ * redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>]
7
+ * redscript check <file>
8
+ * redscript repl
9
+ * redscript version
10
+ */
11
+
12
+ import { compile, check } from './index'
13
+ import { generateCommandBlocks } from './codegen/cmdblock'
14
+ import { compileToStructure } from './codegen/structure'
15
+ import { formatError } from './diagnostics'
16
+ import { startRepl } from './repl'
17
+ import type { OptimizationStats } from './optimizer/commands'
18
+ import * as fs from 'fs'
19
+ import * as path from 'path'
20
+
21
+ // Parse command line arguments
22
+ const args = process.argv.slice(2)
23
+
24
+ function printUsage(): void {
25
+ console.log(`
26
+ RedScript Compiler
27
+
28
+ Usage:
29
+ redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
30
+ redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
+ redscript check <file>
32
+ redscript fmt <file.rs> [file2.rs ...]
33
+ redscript repl
34
+ redscript version
35
+
36
+ Commands:
37
+ compile Compile a RedScript file to a Minecraft datapack
38
+ watch Watch a directory for .rs file changes, recompile, and hot reload
39
+ check Check a RedScript file for errors without generating output
40
+ fmt Auto-format RedScript source files
41
+ repl Start an interactive RedScript REPL
42
+ version Print the RedScript version
43
+
44
+ Options:
45
+ -o, --output <path> Output directory or file path, depending on target
46
+ --output-nbt <file> Output .nbt file path for structure target
47
+ --namespace <ns> Datapack namespace (default: derived from filename)
48
+ --target <target> Output target: datapack (default), cmdblock, or structure
49
+ --stats Print optimizer statistics
50
+ --hot-reload <url> After each successful compile, POST to <url>/reload
51
+ (use with redscript-testharness; e.g. http://localhost:25561)
52
+ -h, --help Show this help message
53
+
54
+ Targets:
55
+ datapack Generate a full Minecraft datapack (default)
56
+ cmdblock Generate JSON structure for command block placement
57
+ structure Generate a Minecraft structure .nbt file with command blocks
58
+ `)
59
+ }
60
+
61
+ function printVersion(): void {
62
+ const packagePath = path.join(__dirname, '..', 'package.json')
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
65
+ console.log(`RedScript v${pkg.version}`)
66
+ } catch {
67
+ console.log('RedScript v0.1.0')
68
+ }
69
+ }
70
+
71
+ function parseArgs(args: string[]): {
72
+ command?: string
73
+ file?: string
74
+ output?: string
75
+ outputNbt?: string
76
+ namespace?: string
77
+ target?: string
78
+ stats?: boolean
79
+ help?: boolean
80
+ hotReload?: string
81
+ } {
82
+ const result: ReturnType<typeof parseArgs> = {}
83
+ let i = 0
84
+
85
+ while (i < args.length) {
86
+ const arg = args[i]
87
+
88
+ if (arg === '-h' || arg === '--help') {
89
+ result.help = true
90
+ i++
91
+ } else if (arg === '-o' || arg === '--output') {
92
+ result.output = args[++i]
93
+ i++
94
+ } else if (arg === '--output-nbt') {
95
+ result.outputNbt = args[++i]
96
+ i++
97
+ } else if (arg === '--namespace') {
98
+ result.namespace = args[++i]
99
+ i++
100
+ } else if (arg === '--target') {
101
+ result.target = args[++i]
102
+ i++
103
+ } else if (arg === '--stats') {
104
+ result.stats = true
105
+ i++
106
+ } else if (arg === '--hot-reload') {
107
+ result.hotReload = args[++i]
108
+ i++
109
+ } else if (!result.command) {
110
+ result.command = arg
111
+ i++
112
+ } else if (!result.file) {
113
+ result.file = arg
114
+ i++
115
+ } else {
116
+ i++
117
+ }
118
+ }
119
+
120
+ return result
121
+ }
122
+
123
+ function deriveNamespace(filePath: string): string {
124
+ const basename = path.basename(filePath, path.extname(filePath))
125
+ // Convert to valid identifier: lowercase, replace non-alphanumeric with underscore
126
+ return basename.toLowerCase().replace(/[^a-z0-9]/g, '_')
127
+ }
128
+
129
+ function printWarnings(warnings: Array<{ code: string; message: string }> | undefined): void {
130
+ if (!warnings || warnings.length === 0) {
131
+ return
132
+ }
133
+
134
+ for (const warning of warnings) {
135
+ console.error(`Warning [${warning.code}]: ${warning.message}`)
136
+ }
137
+ }
138
+
139
+ function formatReduction(before: number, after: number): string {
140
+ if (before === 0) return '0%'
141
+ return `${Math.round(((before - after) / before) * 100)}%`
142
+ }
143
+
144
+ function printOptimizationStats(stats: OptimizationStats | undefined): void {
145
+ if (!stats) return
146
+
147
+ console.log('Optimizations applied:')
148
+ console.log(` LICM: ${stats.licmHoists} reads hoisted from ${stats.licmLoopBodies} loop bodies`)
149
+ console.log(` CSE: ${stats.cseRedundantReads + stats.cseArithmetic} expressions eliminated`)
150
+ console.log(` setblock batching: ${stats.setblockMergedCommands} setblocks -> ${stats.setblockFillCommands} fills (saved ${stats.setblockSavedCommands} commands)`)
151
+ console.log(` dead code: ${stats.deadCodeRemoved} commands removed`)
152
+ console.log(` constant folding: ${stats.constantFolds} constants folded`)
153
+ console.log(` Total mcfunction commands: ${stats.totalCommandsBefore} -> ${stats.totalCommandsAfter} (${formatReduction(stats.totalCommandsBefore, stats.totalCommandsAfter)} reduction)`)
154
+ }
155
+
156
+ function compileCommand(file: string, output: string, namespace: string, target: string = 'datapack', showStats = false): void {
157
+ // Read source file
158
+ if (!fs.existsSync(file)) {
159
+ console.error(`Error: File not found: ${file}`)
160
+ process.exit(1)
161
+ }
162
+
163
+ const source = fs.readFileSync(file, 'utf-8')
164
+
165
+ try {
166
+ if (target === 'cmdblock') {
167
+ const result = compile(source, { namespace, filePath: file })
168
+ printWarnings(result.warnings)
169
+
170
+ // Generate command block JSON
171
+ const hasTick = result.files.some(f => f.path.includes('__tick.mcfunction'))
172
+ const hasLoad = result.files.some(f => f.path.includes('__load.mcfunction'))
173
+ const cmdBlocks = generateCommandBlocks(namespace, hasTick, hasLoad)
174
+
175
+ // Write command block JSON
176
+ fs.mkdirSync(output, { recursive: true })
177
+ const outputFile = path.join(output, `${namespace}_cmdblocks.json`)
178
+ fs.writeFileSync(outputFile, JSON.stringify(cmdBlocks, null, 2))
179
+
180
+ console.log(`✓ Generated command blocks for ${file}`)
181
+ console.log(` Output: ${outputFile}`)
182
+ console.log(` Blocks: ${cmdBlocks.blocks.length}`)
183
+ if (showStats) {
184
+ printOptimizationStats(result.stats)
185
+ }
186
+ } else if (target === 'structure') {
187
+ const structure = compileToStructure(source, namespace, file)
188
+ fs.mkdirSync(path.dirname(output), { recursive: true })
189
+ fs.writeFileSync(output, structure.buffer)
190
+
191
+ console.log(`✓ Generated structure for ${file}`)
192
+ console.log(` Output: ${output}`)
193
+ console.log(` Blocks: ${structure.blockCount}`)
194
+ if (showStats) {
195
+ printOptimizationStats(structure.stats)
196
+ }
197
+ } else {
198
+ const result = compile(source, { namespace, filePath: file })
199
+ printWarnings(result.warnings)
200
+
201
+ // Default: generate datapack
202
+ // Create output directory
203
+ fs.mkdirSync(output, { recursive: true })
204
+
205
+ // Write all files
206
+ for (const dataFile of result.files) {
207
+ const filePath = path.join(output, dataFile.path)
208
+ const dir = path.dirname(filePath)
209
+ fs.mkdirSync(dir, { recursive: true })
210
+ fs.writeFileSync(filePath, dataFile.content)
211
+ }
212
+
213
+ console.log(`✓ Compiled ${file} to ${output}/`)
214
+ console.log(` Namespace: ${namespace}`)
215
+ console.log(` Functions: ${result.ir.functions.length}`)
216
+ console.log(` Files: ${result.files.length}`)
217
+ if (showStats) {
218
+ printOptimizationStats(result.stats)
219
+ }
220
+ }
221
+ } catch (err) {
222
+ console.error(formatError(err as Error, source))
223
+ process.exit(1)
224
+ }
225
+ }
226
+
227
+ function checkCommand(file: string): void {
228
+ // Read source file
229
+ if (!fs.existsSync(file)) {
230
+ console.error(`Error: File not found: ${file}`)
231
+ process.exit(1)
232
+ }
233
+
234
+ const source = fs.readFileSync(file, 'utf-8')
235
+
236
+ const error = check(source, 'redscript', file)
237
+ if (error) {
238
+ console.error(formatError(error, source))
239
+ process.exit(1)
240
+ }
241
+
242
+ console.log(`✓ ${file} is valid`)
243
+ }
244
+
245
+ async function hotReload(url: string): Promise<void> {
246
+ try {
247
+ const res = await fetch(`${url}/reload`, { method: 'POST' })
248
+ if (res.ok) {
249
+ console.log(`🔄 Hot reload sent → ${url}`)
250
+ } else {
251
+ console.warn(`⚠ Hot reload failed: HTTP ${res.status}`)
252
+ }
253
+ } catch (e) {
254
+ console.warn(`⚠ Hot reload failed (is the server running?): ${(e as Error).message}`)
255
+ }
256
+ }
257
+
258
+ function watchCommand(dir: string, output: string, namespace?: string, hotReloadUrl?: string): void {
259
+ // Check if directory exists
260
+ if (!fs.existsSync(dir)) {
261
+ console.error(`Error: Directory not found: ${dir}`)
262
+ process.exit(1)
263
+ }
264
+
265
+ const stat = fs.statSync(dir)
266
+ if (!stat.isDirectory()) {
267
+ console.error(`Error: ${dir} is not a directory`)
268
+ process.exit(1)
269
+ }
270
+
271
+ console.log(`👁 Watching ${dir} for .rs file changes...`)
272
+ console.log(` Output: ${output}`)
273
+ if (hotReloadUrl) console.log(` Hot reload: ${hotReloadUrl}`)
274
+ console.log(` Press Ctrl+C to stop\n`)
275
+
276
+ // Debounce timer
277
+ let debounceTimer: NodeJS.Timeout | null = null
278
+
279
+ // Compile all .rs files in directory
280
+ async function compileAll(): Promise<void> {
281
+ const files = findRsFiles(dir)
282
+ if (files.length === 0) {
283
+ console.log(`⚠ No .rs files found in ${dir}`)
284
+ return
285
+ }
286
+
287
+ let hasErrors = false
288
+ for (const file of files) {
289
+ let source = ''
290
+ try {
291
+ source = fs.readFileSync(file, 'utf-8')
292
+ const ns = namespace ?? deriveNamespace(file)
293
+ const result = compile(source, { namespace: ns, filePath: file })
294
+ printWarnings(result.warnings)
295
+
296
+ // Create output directory
297
+ fs.mkdirSync(output, { recursive: true })
298
+
299
+ // Write all files
300
+ for (const dataFile of result.files) {
301
+ const filePath = path.join(output, dataFile.path)
302
+ const fileDir = path.dirname(filePath)
303
+ fs.mkdirSync(fileDir, { recursive: true })
304
+ fs.writeFileSync(filePath, dataFile.content)
305
+ }
306
+
307
+ const timestamp = new Date().toLocaleTimeString()
308
+ console.log(`✓ [${timestamp}] Compiled ${file} (${result.files.length} files)`)
309
+ } catch (err) {
310
+ hasErrors = true
311
+ const timestamp = new Date().toLocaleTimeString()
312
+ console.error(`✗ [${timestamp}] ${formatError(err as Error, source)}`)
313
+ }
314
+ }
315
+
316
+ if (!hasErrors) {
317
+ if (hotReloadUrl) await hotReload(hotReloadUrl)
318
+ console.log('')
319
+ }
320
+ }
321
+
322
+ // Find all .rs files recursively
323
+ function findRsFiles(directory: string): string[] {
324
+ const results: string[] = []
325
+ const entries = fs.readdirSync(directory, { withFileTypes: true })
326
+
327
+ for (const entry of entries) {
328
+ const fullPath = path.join(directory, entry.name)
329
+ if (entry.isDirectory()) {
330
+ results.push(...findRsFiles(fullPath))
331
+ } else if (entry.isFile() && entry.name.endsWith('.rs')) {
332
+ results.push(fullPath)
333
+ }
334
+ }
335
+
336
+ return results
337
+ }
338
+
339
+ // Initial compile
340
+ void compileAll()
341
+
342
+ // Watch for changes
343
+ fs.watch(dir, { recursive: true }, (eventType, filename) => {
344
+ if (filename && filename.endsWith('.rs')) {
345
+ // Debounce rapid changes
346
+ if (debounceTimer) {
347
+ clearTimeout(debounceTimer)
348
+ }
349
+ debounceTimer = setTimeout(() => {
350
+ console.log(`📝 Change detected: ${filename}`)
351
+ void compileAll()
352
+ }, 100)
353
+ }
354
+ })
355
+ }
356
+
357
+ // Main
358
+ const parsed = parseArgs(args)
359
+
360
+ async function main(): Promise<void> {
361
+ if (parsed.help || !parsed.command) {
362
+ printUsage()
363
+ process.exit(parsed.help ? 0 : 1)
364
+ }
365
+
366
+ switch (parsed.command) {
367
+ case 'compile':
368
+ if (!parsed.file) {
369
+ console.error('Error: No input file specified')
370
+ printUsage()
371
+ process.exit(1)
372
+ }
373
+ {
374
+ const namespace = parsed.namespace ?? deriveNamespace(parsed.file)
375
+ const target = parsed.target ?? 'datapack'
376
+ const output = target === 'structure'
377
+ ? (parsed.outputNbt ?? parsed.output ?? `./${namespace}.nbt`)
378
+ : (parsed.output ?? './dist')
379
+
380
+ compileCommand(
381
+ parsed.file,
382
+ output,
383
+ namespace,
384
+ target,
385
+ parsed.stats
386
+ )
387
+ }
388
+ break
389
+
390
+ case 'watch':
391
+ if (!parsed.file) {
392
+ console.error('Error: No directory specified')
393
+ printUsage()
394
+ process.exit(1)
395
+ }
396
+ watchCommand(
397
+ parsed.file,
398
+ parsed.output ?? './dist',
399
+ parsed.namespace,
400
+ parsed.hotReload
401
+ )
402
+ break
403
+
404
+ case 'check':
405
+ if (!parsed.file) {
406
+ console.error('Error: No input file specified')
407
+ printUsage()
408
+ process.exit(1)
409
+ }
410
+ checkCommand(parsed.file)
411
+ break
412
+
413
+ case 'fmt':
414
+ case 'format': {
415
+ const files = args.filter(a => a.endsWith('.rs'))
416
+ if (files.length === 0) {
417
+ console.error('Usage: redscript fmt <file.rs> [file2.rs ...]')
418
+ process.exit(1)
419
+ }
420
+ const { format } = require('./formatter')
421
+ for (const file of files) {
422
+ const content = fs.readFileSync(file, 'utf8')
423
+ const formatted = format(content)
424
+ fs.writeFileSync(file, formatted)
425
+ console.log(`Formatted: ${file}`)
426
+ }
427
+ break
428
+ }
429
+
430
+ case 'repl':
431
+ await startRepl(parsed.namespace ?? 'repl')
432
+ break
433
+
434
+ case 'version':
435
+ printVersion()
436
+ break
437
+
438
+ default:
439
+ console.error(`Error: Unknown command '${parsed.command}'`)
440
+ printUsage()
441
+ process.exit(1)
442
+ }
443
+ }
444
+
445
+ void main()
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Command Block Target
3
+ *
4
+ * Generates a JSON structure representing command blocks that can be
5
+ * placed in Minecraft to run the compiled datapack.
6
+ */
7
+
8
+ export interface CommandBlock {
9
+ type: 'repeat' | 'impulse' | 'chain'
10
+ command: string
11
+ pos: [number, number, number]
12
+ auto?: boolean
13
+ conditional?: boolean
14
+ }
15
+
16
+ export interface CommandBlockStructure {
17
+ format: 'redscript-cmdblock-v1'
18
+ namespace: string
19
+ blocks: CommandBlock[]
20
+ }
21
+
22
+ /**
23
+ * Generate a command block structure JSON for a given namespace.
24
+ *
25
+ * Creates:
26
+ * - 1 × Repeat block: function <namespace>:__tick
27
+ * - 1 × Impulse block (auto): function <namespace>:__load
28
+ */
29
+ export function generateCommandBlocks(
30
+ namespace: string,
31
+ hasTick: boolean,
32
+ hasLoad: boolean
33
+ ): CommandBlockStructure {
34
+ const blocks: CommandBlock[] = []
35
+ let x = 0
36
+
37
+ // Load block - impulse with auto (runs once when placed)
38
+ if (hasLoad) {
39
+ blocks.push({
40
+ type: 'impulse',
41
+ command: `function ${namespace}:__load`,
42
+ pos: [x, 0, 0],
43
+ auto: true,
44
+ })
45
+ x++
46
+ }
47
+
48
+ // Tick block - repeat (runs every tick)
49
+ if (hasTick) {
50
+ blocks.push({
51
+ type: 'repeat',
52
+ command: `function ${namespace}:__tick`,
53
+ pos: [x, 0, 0],
54
+ })
55
+ x++
56
+ }
57
+
58
+ return {
59
+ format: 'redscript-cmdblock-v1',
60
+ namespace,
61
+ blocks,
62
+ }
63
+ }