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
@@ -0,0 +1,367 @@
1
+ import * as fs from 'fs'
2
+
3
+ interface BrigadierFile {
4
+ root: BrigadierNode
5
+ }
6
+
7
+ interface BrigadierNode {
8
+ type?: 'literal' | 'argument' | 'root'
9
+ name?: string
10
+ executable?: boolean
11
+ children?: BrigadierNode[]
12
+ redirects?: string[]
13
+ parser?: {
14
+ parser: string
15
+ modifier?: {
16
+ type?: string
17
+ } | null
18
+ }
19
+ }
20
+
21
+ export interface ValidationResult {
22
+ valid: boolean
23
+ error?: string
24
+ }
25
+
26
+ const FUNCTION_ID_RE = /^[0-9a-z_.-]+:[0-9a-z_./-]+$/i
27
+ const INTEGER_RE = /^-?\d+$/
28
+ const SCORE_RANGE_RE = /^-?\d+\.\.$|^\.\.-?\d+$|^-?\d+\.\.-?\d+$|^-?\d+$/
29
+ const COMMENT_PREFIXES = [
30
+ '# RedScript runtime init',
31
+ '# block:',
32
+ '# RedScript tick dispatcher',
33
+ ]
34
+ const SCOREBOARD_PLAYER_ACTIONS = new Set(['set', 'add', 'remove', 'get', 'operation', 'enable'])
35
+ const SCOREBOARD_OPERATIONS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '<', '>', '><'])
36
+
37
+ export class MCCommandValidator {
38
+ private readonly root: BrigadierNode
39
+ private readonly rootChildren: BrigadierNode[]
40
+
41
+ constructor(commandsPath: string) {
42
+ const parsed = JSON.parse(fs.readFileSync(commandsPath, 'utf-8')) as BrigadierFile
43
+ this.root = parsed.root
44
+ this.rootChildren = parsed.root.children ?? []
45
+ }
46
+
47
+ validate(line: string): ValidationResult {
48
+ const trimmed = line.trim()
49
+ if (!trimmed || trimmed.startsWith('#') || COMMENT_PREFIXES.some(prefix => trimmed.startsWith(prefix))) {
50
+ return { valid: true }
51
+ }
52
+
53
+ const tokens = tokenize(trimmed)
54
+ if (tokens.length === 0) {
55
+ return { valid: true }
56
+ }
57
+
58
+ if (!this.hasRootCommand(tokens[0])) {
59
+ return { valid: false, error: `Unknown root command: ${tokens[0]}` }
60
+ }
61
+
62
+ switch (tokens[0]) {
63
+ case 'execute':
64
+ return this.validateExecute(tokens)
65
+ case 'scoreboard':
66
+ return this.validateScoreboard(tokens)
67
+ case 'function':
68
+ return this.validateFunction(tokens)
69
+ case 'data':
70
+ return this.validateData(tokens)
71
+ case 'return':
72
+ return this.validateReturn(tokens)
73
+ default:
74
+ return this.validateAgainstTree(tokens)
75
+ }
76
+ }
77
+
78
+ private hasRootCommand(command: string): boolean {
79
+ return this.rootChildren.some(child => child.type === 'literal' && child.name === command)
80
+ }
81
+
82
+ private validateExecute(tokens: string[]): ValidationResult {
83
+ const runIndex = tokens.indexOf('run')
84
+ if (runIndex === 1 || runIndex === tokens.length - 1) {
85
+ return { valid: false, error: 'Malformed execute run clause' }
86
+ }
87
+
88
+ if (runIndex !== -1) {
89
+ const chainResult = this.validateAgainstTree(tokens.slice(0, runIndex))
90
+ if (!chainResult.valid) {
91
+ return chainResult
92
+ }
93
+
94
+ return this.validate(tokens.slice(runIndex + 1).join(' '))
95
+ }
96
+
97
+ return this.validateAgainstTree(tokens)
98
+ }
99
+
100
+ private validateScoreboard(tokens: string[]): ValidationResult {
101
+ if (tokens[1] === 'objectives' && tokens[2] === 'add') {
102
+ if (tokens.length < 5) {
103
+ return { valid: false, error: 'scoreboard objectives add requires name and criteria' }
104
+ }
105
+ return this.validateAgainstTree(tokens)
106
+ }
107
+
108
+ if (tokens[1] !== 'players' || !SCOREBOARD_PLAYER_ACTIONS.has(tokens[2] ?? '')) {
109
+ return this.validateAgainstTree(tokens)
110
+ }
111
+
112
+ const action = tokens[2]
113
+ if (action === 'enable') {
114
+ if (tokens.length !== 5) {
115
+ return { valid: false, error: 'scoreboard players enable requires target and objective' }
116
+ }
117
+ return this.validateAgainstTree(tokens)
118
+ }
119
+
120
+ if (action === 'get') {
121
+ if (tokens.length !== 5) {
122
+ return { valid: false, error: 'scoreboard players get requires target and objective' }
123
+ }
124
+ return this.validateAgainstTree(tokens)
125
+ }
126
+
127
+ if (action === 'operation') {
128
+ if (tokens.length !== 8) {
129
+ return { valid: false, error: 'scoreboard players operation requires 5 operands' }
130
+ }
131
+ if (!SCOREBOARD_OPERATIONS.has(tokens[5])) {
132
+ return { valid: false, error: `Unknown scoreboard operation: ${tokens[5]}` }
133
+ }
134
+ return this.validateAgainstTree(tokens)
135
+ }
136
+
137
+ if (tokens.length !== 6) {
138
+ return { valid: false, error: `scoreboard players ${action} requires target, objective, and value` }
139
+ }
140
+
141
+ if (!INTEGER_RE.test(tokens[5])) {
142
+ return { valid: false, error: `Expected integer value, got: ${tokens[5]}` }
143
+ }
144
+
145
+ return this.validateAgainstTree(tokens)
146
+ }
147
+
148
+ private validateFunction(tokens: string[]): ValidationResult {
149
+ if (tokens.length !== 2 || !FUNCTION_ID_RE.test(tokens[1])) {
150
+ return { valid: false, error: 'function requires a namespaced function id' }
151
+ }
152
+
153
+ return this.validateAgainstTree(tokens)
154
+ }
155
+
156
+ private validateData(tokens: string[]): ValidationResult {
157
+ if (tokens.length < 5) {
158
+ return { valid: false, error: 'data command is incomplete' }
159
+ }
160
+
161
+ const action = tokens[1]
162
+ if (!['get', 'modify', 'merge', 'remove'].includes(action)) {
163
+ return this.validateAgainstTree(tokens)
164
+ }
165
+
166
+ const targetType = tokens[2]
167
+ if (!['storage', 'entity', 'block'].includes(targetType)) {
168
+ return { valid: false, error: `Unsupported data target: ${targetType}` }
169
+ }
170
+
171
+ if (action === 'get') {
172
+ if (tokens.length < 5) {
173
+ return { valid: false, error: 'data get requires target and path' }
174
+ }
175
+ if (tokens[5] && !isNumberish(tokens[5])) {
176
+ return { valid: false, error: `Invalid data get scale: ${tokens[5]}` }
177
+ }
178
+ return this.validateAgainstTree(tokens)
179
+ }
180
+
181
+ if (action === 'modify') {
182
+ if (tokens.length < 7) {
183
+ return { valid: false, error: 'data modify is incomplete' }
184
+ }
185
+ if (!['set', 'append', 'prepend', 'insert', 'merge'].includes(tokens[5])) {
186
+ return { valid: false, error: `Unsupported data modify mode: ${tokens[5]}` }
187
+ }
188
+ return this.validateAgainstTree(tokens)
189
+ }
190
+
191
+ return this.validateAgainstTree(tokens)
192
+ }
193
+
194
+ private validateReturn(tokens: string[]): ValidationResult {
195
+ if (tokens.length < 2) {
196
+ return { valid: false, error: 'return requires a value or run clause' }
197
+ }
198
+
199
+ if (tokens[1] === 'run') {
200
+ if (tokens.length < 3) {
201
+ return { valid: false, error: 'return run requires an inner command' }
202
+ }
203
+ return this.validate(tokens.slice(2).join(' '))
204
+ }
205
+
206
+ if (!INTEGER_RE.test(tokens[1])) {
207
+ return { valid: false, error: `Invalid return value: ${tokens[1]}` }
208
+ }
209
+
210
+ return this.validateAgainstTree(tokens)
211
+ }
212
+
213
+ private validateAgainstTree(tokens: string[]): ValidationResult {
214
+ const memo = new Map<string, boolean>()
215
+ const isValid = walk(this.root, tokens, 0, memo, this.rootChildren)
216
+
217
+ return isValid
218
+ ? { valid: true }
219
+ : { valid: false, error: `Command does not match Brigadier tree: ${tokens.join(' ')}` }
220
+ }
221
+ }
222
+
223
+ function walk(
224
+ node: BrigadierNode,
225
+ tokens: string[],
226
+ index: number,
227
+ memo: Map<string, boolean>,
228
+ rootChildren: BrigadierNode[]
229
+ ): boolean {
230
+ const key = `${node.name ?? '<root>'}:${index}`
231
+ const cached = memo.get(key)
232
+ if (cached !== undefined) {
233
+ return cached
234
+ }
235
+
236
+ if (index === tokens.length) {
237
+ const done = node.executable === true || (node.children ?? []).length === 0
238
+ memo.set(key, done)
239
+ return done
240
+ }
241
+
242
+ const children = node.children ?? []
243
+ for (const child of children) {
244
+ if (child.type === 'literal') {
245
+ if (child.name === tokens[index] && walk(child, tokens, index + 1, memo, rootChildren)) {
246
+ memo.set(key, true)
247
+ return true
248
+ }
249
+ continue
250
+ }
251
+
252
+ if (child.type !== 'argument') {
253
+ continue
254
+ }
255
+
256
+ const parser = child.parser?.parser
257
+ const modifier = child.parser?.modifier?.type
258
+ if (parserConsumesRest(parser, modifier)) {
259
+ const done = child.executable === true || (child.children ?? []).length === 0
260
+ if (done) {
261
+ memo.set(key, true)
262
+ return true
263
+ }
264
+ }
265
+
266
+ const width = parserTokenWidth(parser, tokens, index)
267
+ if (width === null) {
268
+ continue
269
+ }
270
+
271
+ const nextIndex = index + width
272
+ if (walk(child, tokens, nextIndex, memo, rootChildren)) {
273
+ memo.set(key, true)
274
+ return true
275
+ }
276
+
277
+ for (const redirect of child.redirects ?? []) {
278
+ const target = rootChildren.find(candidate => candidate.name === redirect)
279
+ if (target && walk(target, tokens, nextIndex, memo, rootChildren)) {
280
+ memo.set(key, true)
281
+ return true
282
+ }
283
+ }
284
+ }
285
+
286
+ memo.set(key, false)
287
+ return false
288
+ }
289
+
290
+ function parserConsumesRest(parser?: string, modifier?: string): boolean {
291
+ return (
292
+ (parser === 'brigadier:string' && modifier === 'greedy') ||
293
+ parser === 'minecraft:message'
294
+ )
295
+ }
296
+
297
+ function parserTokenWidth(parser: string | undefined, tokens: string[], index: number): number | null {
298
+ switch (parser) {
299
+ case 'minecraft:vec3':
300
+ case 'minecraft:block_pos':
301
+ return index + 3 <= tokens.length ? 3 : null
302
+ case 'minecraft:vec2':
303
+ case 'minecraft:column_pos':
304
+ case 'minecraft:rotation':
305
+ return index + 2 <= tokens.length ? 2 : null
306
+ default:
307
+ return index < tokens.length ? 1 : null
308
+ }
309
+ }
310
+
311
+ function tokenize(line: string): string[] {
312
+ const tokens: string[] = []
313
+ let current = ''
314
+ let quote: '"' | '\'' | null = null
315
+ let escape = false
316
+ let bracketDepth = 0
317
+ let braceDepth = 0
318
+
319
+ for (const char of line) {
320
+ if (escape) {
321
+ current += char
322
+ escape = false
323
+ continue
324
+ }
325
+
326
+ if (quote) {
327
+ current += char
328
+ if (char === '\\') {
329
+ escape = true
330
+ } else if (char === quote) {
331
+ quote = null
332
+ }
333
+ continue
334
+ }
335
+
336
+ if (char === '"' || char === '\'') {
337
+ quote = char
338
+ current += char
339
+ continue
340
+ }
341
+
342
+ if (char === '[') bracketDepth += 1
343
+ if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1)
344
+ if (char === '{') braceDepth += 1
345
+ if (char === '}') braceDepth = Math.max(0, braceDepth - 1)
346
+
347
+ if (/\s/.test(char) && bracketDepth === 0 && braceDepth === 0) {
348
+ if (current) {
349
+ tokens.push(current)
350
+ current = ''
351
+ }
352
+ continue
353
+ }
354
+
355
+ current += char
356
+ }
357
+
358
+ if (current) {
359
+ tokens.push(current)
360
+ }
361
+
362
+ return tokens
363
+ }
364
+
365
+ function isNumberish(value: string): boolean {
366
+ return /^-?\d+(\.\d+)?$/.test(value) || SCORE_RANGE_RE.test(value)
367
+ }
@@ -0,0 +1,321 @@
1
+ export const enum TagType {
2
+ End = 0,
3
+ Byte = 1,
4
+ Short = 2,
5
+ Int = 3,
6
+ Long = 4,
7
+ Float = 5,
8
+ Double = 6,
9
+ ByteArray = 7,
10
+ String = 8,
11
+ List = 9,
12
+ Compound = 10,
13
+ IntArray = 11,
14
+ LongArray = 12,
15
+ }
16
+
17
+ export type EndTag = { type: TagType.End }
18
+ export type ByteTag = { type: TagType.Byte; value: number }
19
+ export type ShortTag = { type: TagType.Short; value: number }
20
+ export type IntTag = { type: TagType.Int; value: number }
21
+ export type LongTag = { type: TagType.Long; value: bigint }
22
+ export type FloatTag = { type: TagType.Float; value: number }
23
+ export type DoubleTag = { type: TagType.Double; value: number }
24
+ export type ByteArrayTag = { type: TagType.ByteArray; value: Int8Array }
25
+ export type StringTag = { type: TagType.String; value: string }
26
+ export type ListTag = { type: TagType.List; elementType: TagType; items: NbtTag[] }
27
+ export type CompoundTag = { type: TagType.Compound; entries: Map<string, NbtTag> }
28
+ export type IntArrayTag = { type: TagType.IntArray; value: Int32Array }
29
+ export type LongArrayTag = { type: TagType.LongArray; value: BigInt64Array }
30
+
31
+ export type NbtTag =
32
+ | EndTag
33
+ | ByteTag
34
+ | ShortTag
35
+ | IntTag
36
+ | LongTag
37
+ | FloatTag
38
+ | DoubleTag
39
+ | ByteArrayTag
40
+ | StringTag
41
+ | ListTag
42
+ | CompoundTag
43
+ | IntArrayTag
44
+ | LongArrayTag
45
+
46
+ function encodeModifiedUtf8(value: string): Buffer {
47
+ const bytes: number[] = []
48
+
49
+ for (let i = 0; i < value.length; i++) {
50
+ const codeUnit = value.charCodeAt(i)
51
+
52
+ if (codeUnit !== 0 && codeUnit <= 0x7f) {
53
+ bytes.push(codeUnit)
54
+ continue
55
+ }
56
+
57
+ if (codeUnit <= 0x07ff) {
58
+ bytes.push(
59
+ 0xc0 | ((codeUnit >> 6) & 0x1f),
60
+ 0x80 | (codeUnit & 0x3f)
61
+ )
62
+ continue
63
+ }
64
+
65
+ bytes.push(
66
+ 0xe0 | ((codeUnit >> 12) & 0x0f),
67
+ 0x80 | ((codeUnit >> 6) & 0x3f),
68
+ 0x80 | (codeUnit & 0x3f)
69
+ )
70
+ }
71
+
72
+ if (bytes.length > 0xffff) {
73
+ throw new Error(`NBT string is too long: ${bytes.length} bytes`)
74
+ }
75
+
76
+ const buffer = Buffer.allocUnsafe(2 + bytes.length)
77
+ buffer.writeUInt16BE(bytes.length, 0)
78
+ for (let i = 0; i < bytes.length; i++) {
79
+ buffer[2 + i] = bytes[i]
80
+ }
81
+ return buffer
82
+ }
83
+
84
+ function decodeModifiedUtf8(buffer: Buffer, offset: number): { value: string; offset: number } {
85
+ const byteLength = buffer.readUInt16BE(offset)
86
+ offset += 2
87
+
88
+ const codeUnits: number[] = []
89
+ const end = offset + byteLength
90
+
91
+ while (offset < end) {
92
+ const first = buffer[offset++]
93
+
94
+ if ((first & 0x80) === 0) {
95
+ codeUnits.push(first)
96
+ continue
97
+ }
98
+
99
+ if ((first & 0xe0) === 0xc0) {
100
+ const second = buffer[offset++]
101
+ codeUnits.push(((first & 0x1f) << 6) | (second & 0x3f))
102
+ continue
103
+ }
104
+
105
+ const second = buffer[offset++]
106
+ const third = buffer[offset++]
107
+ codeUnits.push(
108
+ ((first & 0x0f) << 12) |
109
+ ((second & 0x3f) << 6) |
110
+ (third & 0x3f)
111
+ )
112
+ }
113
+
114
+ return {
115
+ value: String.fromCharCode(...codeUnits),
116
+ offset,
117
+ }
118
+ }
119
+
120
+ function writePayload(tag: NbtTag): Buffer {
121
+ switch (tag.type) {
122
+ case TagType.End:
123
+ return Buffer.alloc(0)
124
+ case TagType.Byte: {
125
+ const buffer = Buffer.allocUnsafe(1)
126
+ buffer.writeInt8(tag.value, 0)
127
+ return buffer
128
+ }
129
+ case TagType.Short: {
130
+ const buffer = Buffer.allocUnsafe(2)
131
+ buffer.writeInt16BE(tag.value, 0)
132
+ return buffer
133
+ }
134
+ case TagType.Int: {
135
+ const buffer = Buffer.allocUnsafe(4)
136
+ buffer.writeInt32BE(tag.value, 0)
137
+ return buffer
138
+ }
139
+ case TagType.Long: {
140
+ const buffer = Buffer.allocUnsafe(8)
141
+ buffer.writeBigInt64BE(tag.value, 0)
142
+ return buffer
143
+ }
144
+ case TagType.Float: {
145
+ const buffer = Buffer.allocUnsafe(4)
146
+ buffer.writeFloatBE(tag.value, 0)
147
+ return buffer
148
+ }
149
+ case TagType.Double: {
150
+ const buffer = Buffer.allocUnsafe(8)
151
+ buffer.writeDoubleBE(tag.value, 0)
152
+ return buffer
153
+ }
154
+ case TagType.ByteArray: {
155
+ const header = Buffer.allocUnsafe(4)
156
+ header.writeInt32BE(tag.value.length, 0)
157
+ return Buffer.concat([header, Buffer.from(tag.value)])
158
+ }
159
+ case TagType.String:
160
+ return encodeModifiedUtf8(tag.value)
161
+ case TagType.List: {
162
+ const header = Buffer.allocUnsafe(5)
163
+ header.writeUInt8(tag.elementType, 0)
164
+ header.writeInt32BE(tag.items.length, 1)
165
+ return Buffer.concat([header, ...tag.items.map(writePayload)])
166
+ }
167
+ case TagType.Compound: {
168
+ const parts: Buffer[] = []
169
+ for (const [name, entry] of tag.entries) {
170
+ parts.push(writeNamedTag(entry, name))
171
+ }
172
+ parts.push(Buffer.from([TagType.End]))
173
+ return Buffer.concat(parts)
174
+ }
175
+ case TagType.IntArray: {
176
+ const header = Buffer.allocUnsafe(4 + tag.value.length * 4)
177
+ header.writeInt32BE(tag.value.length, 0)
178
+ for (let i = 0; i < tag.value.length; i++) {
179
+ header.writeInt32BE(tag.value[i], 4 + i * 4)
180
+ }
181
+ return header
182
+ }
183
+ case TagType.LongArray: {
184
+ const header = Buffer.allocUnsafe(4 + tag.value.length * 8)
185
+ header.writeInt32BE(tag.value.length, 0)
186
+ for (let i = 0; i < tag.value.length; i++) {
187
+ header.writeBigInt64BE(tag.value[i], 4 + i * 8)
188
+ }
189
+ return header
190
+ }
191
+ }
192
+ }
193
+
194
+ function writeNamedTag(tag: NbtTag, name: string): Buffer {
195
+ if (tag.type === TagType.End) {
196
+ throw new Error('TAG_End cannot be written as a named tag')
197
+ }
198
+
199
+ const nameBuffer = encodeModifiedUtf8(name)
200
+ return Buffer.concat([
201
+ Buffer.from([tag.type]),
202
+ nameBuffer,
203
+ writePayload(tag),
204
+ ])
205
+ }
206
+
207
+ function readPayload(type: TagType, buffer: Buffer, offset: number): { tag: NbtTag; offset: number } {
208
+ switch (type) {
209
+ case TagType.End:
210
+ return { tag: { type: TagType.End }, offset }
211
+ case TagType.Byte:
212
+ return { tag: { type: type, value: buffer.readInt8(offset) }, offset: offset + 1 }
213
+ case TagType.Short:
214
+ return { tag: { type: type, value: buffer.readInt16BE(offset) }, offset: offset + 2 }
215
+ case TagType.Int:
216
+ return { tag: { type: type, value: buffer.readInt32BE(offset) }, offset: offset + 4 }
217
+ case TagType.Long:
218
+ return { tag: { type: type, value: buffer.readBigInt64BE(offset) }, offset: offset + 8 }
219
+ case TagType.Float:
220
+ return { tag: { type: type, value: buffer.readFloatBE(offset) }, offset: offset + 4 }
221
+ case TagType.Double:
222
+ return { tag: { type: type, value: buffer.readDoubleBE(offset) }, offset: offset + 8 }
223
+ case TagType.ByteArray: {
224
+ const length = buffer.readInt32BE(offset)
225
+ offset += 4
226
+ const value = new Int8Array(length)
227
+ for (let i = 0; i < length; i++) {
228
+ value[i] = buffer.readInt8(offset + i)
229
+ }
230
+ return { tag: { type, value }, offset: offset + length }
231
+ }
232
+ case TagType.String: {
233
+ const decoded = decodeModifiedUtf8(buffer, offset)
234
+ return { tag: { type, value: decoded.value }, offset: decoded.offset }
235
+ }
236
+ case TagType.List: {
237
+ const elementType = buffer.readUInt8(offset) as TagType
238
+ const length = buffer.readInt32BE(offset + 1)
239
+ offset += 5
240
+ const items: NbtTag[] = []
241
+ for (let i = 0; i < length; i++) {
242
+ const parsed = readPayload(elementType, buffer, offset)
243
+ items.push(parsed.tag)
244
+ offset = parsed.offset
245
+ }
246
+ return { tag: { type, elementType, items }, offset }
247
+ }
248
+ case TagType.Compound: {
249
+ const entries = new Map<string, NbtTag>()
250
+ while (true) {
251
+ const entryType = buffer.readUInt8(offset) as TagType
252
+ offset += 1
253
+ if (entryType === TagType.End) break
254
+ const name = decodeModifiedUtf8(buffer, offset)
255
+ offset = name.offset
256
+ const parsed = readPayload(entryType, buffer, offset)
257
+ entries.set(name.value, parsed.tag)
258
+ offset = parsed.offset
259
+ }
260
+ return { tag: { type, entries }, offset }
261
+ }
262
+ case TagType.IntArray: {
263
+ const length = buffer.readInt32BE(offset)
264
+ offset += 4
265
+ const value = new Int32Array(length)
266
+ for (let i = 0; i < length; i++) {
267
+ value[i] = buffer.readInt32BE(offset + i * 4)
268
+ }
269
+ return { tag: { type, value }, offset: offset + length * 4 }
270
+ }
271
+ case TagType.LongArray: {
272
+ const length = buffer.readInt32BE(offset)
273
+ offset += 4
274
+ const value = new BigInt64Array(length)
275
+ for (let i = 0; i < length; i++) {
276
+ value[i] = buffer.readBigInt64BE(offset + i * 8)
277
+ }
278
+ return { tag: { type, value }, offset: offset + length * 8 }
279
+ }
280
+ default:
281
+ throw new Error(`Unsupported NBT tag type: ${type}`)
282
+ }
283
+ }
284
+
285
+ export function writeNbt(tag: NbtTag, name: string): Buffer {
286
+ return writeNamedTag(tag, name)
287
+ }
288
+
289
+ export function readNbt(buffer: Buffer): { name: string; tag: NbtTag } {
290
+ let offset = 0
291
+ const type = buffer.readUInt8(offset) as TagType
292
+ offset += 1
293
+
294
+ if (type === TagType.End) {
295
+ throw new Error('Invalid root tag: TAG_End')
296
+ }
297
+
298
+ const decodedName = decodeModifiedUtf8(buffer, offset)
299
+ offset = decodedName.offset
300
+ const parsed = readPayload(type, buffer, offset)
301
+
302
+ return {
303
+ name: decodedName.value,
304
+ tag: parsed.tag,
305
+ }
306
+ }
307
+
308
+ export const nbt = {
309
+ byte: (value: number): ByteTag => ({ type: TagType.Byte, value }),
310
+ short: (value: number): ShortTag => ({ type: TagType.Short, value }),
311
+ int: (value: number): IntTag => ({ type: TagType.Int, value }),
312
+ long: (value: bigint): LongTag => ({ type: TagType.Long, value }),
313
+ float: (value: number): FloatTag => ({ type: TagType.Float, value }),
314
+ double: (value: number): DoubleTag => ({ type: TagType.Double, value }),
315
+ string: (value: string): StringTag => ({ type: TagType.String, value }),
316
+ list: (elementType: TagType, items: NbtTag[]): ListTag => ({ type: TagType.List, elementType, items }),
317
+ compound: (entries: Record<string, NbtTag>): CompoundTag =>
318
+ ({ type: TagType.Compound, entries: new Map(Object.entries(entries)) }),
319
+ intArray: (values: number[]): IntArrayTag => ({ type: TagType.IntArray, value: Int32Array.from(values) }),
320
+ byteArray: (values: number[]): ByteArrayTag => ({ type: TagType.ByteArray, value: Int8Array.from(values) }),
321
+ }