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,1403 @@
1
+ /**
2
+ * MCRuntime - Minecraft Command Runtime Simulator
3
+ *
4
+ * A TypeScript interpreter that simulates the subset of MC commands that
5
+ * RedScript generates, allowing behavioral testing without a real server.
6
+ */
7
+
8
+ import { compile as rsCompile } from '../compile'
9
+ import * as fs from 'fs'
10
+ import * as path from 'path'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface Entity {
17
+ id: string
18
+ tags: Set<string>
19
+ scores: Map<string, number>
20
+ selector: string
21
+ type?: string
22
+ position?: { x: number; y: number; z: number }
23
+ }
24
+
25
+ interface Range {
26
+ min: number
27
+ max: number
28
+ }
29
+
30
+ interface SelectorFilters {
31
+ tag?: string[]
32
+ notTag?: string[]
33
+ type?: string[]
34
+ notType?: string[]
35
+ limit?: number
36
+ scores?: Map<string, Range>
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Selector & Range Parsing
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function parseRange(s: string): Range {
44
+ if (s.includes('..')) {
45
+ const [left, right] = s.split('..')
46
+ return {
47
+ min: left === '' ? -Infinity : parseInt(left, 10),
48
+ max: right === '' ? Infinity : parseInt(right, 10),
49
+ }
50
+ }
51
+ const val = parseInt(s, 10)
52
+ return { min: val, max: val }
53
+ }
54
+
55
+ function matchesRange(value: number, range: Range): boolean {
56
+ return value >= range.min && value <= range.max
57
+ }
58
+
59
+ function canonicalEntityType(entityType: string): string {
60
+ return entityType.includes(':') ? entityType : `minecraft:${entityType}`
61
+ }
62
+
63
+ function parseFilters(content: string): SelectorFilters {
64
+ const filters: SelectorFilters = {
65
+ tag: [],
66
+ notTag: [],
67
+ type: [],
68
+ notType: [],
69
+ }
70
+
71
+ if (!content) return filters
72
+
73
+ // Handle scores={...} separately
74
+ let processed = content
75
+ const scoresMatch = content.match(/scores=\{([^}]*)\}/)
76
+ if (scoresMatch) {
77
+ filters.scores = new Map()
78
+ const scoresPart = scoresMatch[1]
79
+ const scoreEntries = scoresPart.split(',')
80
+ for (const entry of scoreEntries) {
81
+ const [obj, range] = entry.split('=')
82
+ if (obj && range) {
83
+ filters.scores.set(obj.trim(), parseRange(range.trim()))
84
+ }
85
+ }
86
+ processed = content.replace(/,?scores=\{[^}]*\},?/, ',').replace(/^,|,$/g, '')
87
+ }
88
+
89
+ // Parse remaining filters
90
+ const parts = processed.split(',').filter(p => p.trim())
91
+ for (const part of parts) {
92
+ const [key, value] = part.split('=').map(s => s.trim())
93
+ if (key === 'tag') {
94
+ if (value.startsWith('!')) {
95
+ filters.notTag!.push(value.slice(1))
96
+ } else {
97
+ filters.tag!.push(value)
98
+ }
99
+ } else if (key === 'type') {
100
+ if (value.startsWith('!')) {
101
+ filters.notType!.push(value.slice(1))
102
+ } else {
103
+ filters.type!.push(value)
104
+ }
105
+ } else if (key === 'limit') {
106
+ filters.limit = parseInt(value, 10)
107
+ }
108
+ }
109
+
110
+ return filters
111
+ }
112
+
113
+ function matchesFilters(entity: Entity, filters: SelectorFilters, objective: string = 'rs'): boolean {
114
+ // Check required tags
115
+ for (const tag of filters.tag || []) {
116
+ if (!entity.tags.has(tag)) return false
117
+ }
118
+
119
+ // Check excluded tags
120
+ for (const notTag of filters.notTag || []) {
121
+ if (entity.tags.has(notTag)) return false
122
+ }
123
+
124
+ // Check types
125
+ if ((filters.type?.length ?? 0) > 0) {
126
+ const entityType = canonicalEntityType(entity.type ?? 'minecraft:armor_stand')
127
+ const allowedTypes = filters.type!.map(canonicalEntityType)
128
+ if (!allowedTypes.includes(entityType)) {
129
+ return false
130
+ }
131
+ }
132
+ for (const notType of filters.notType || []) {
133
+ const entityType = canonicalEntityType(entity.type ?? 'minecraft:armor_stand')
134
+ if (canonicalEntityType(notType) === entityType) {
135
+ return false
136
+ }
137
+ }
138
+
139
+ // Check scores
140
+ if (filters.scores) {
141
+ for (const [obj, range] of filters.scores) {
142
+ const score = entity.scores.get(obj) ?? 0
143
+ if (!matchesRange(score, range)) return false
144
+ }
145
+ }
146
+
147
+ return true
148
+ }
149
+
150
+ function parseSelector(
151
+ sel: string,
152
+ entities: Entity[],
153
+ executor?: Entity
154
+ ): Entity[] {
155
+ // Handle @s
156
+ if (sel === '@s') {
157
+ return executor ? [executor] : []
158
+ }
159
+
160
+ // Handle bare selectors
161
+ if (sel === '@e' || sel === '@a') {
162
+ return [...entities]
163
+ }
164
+
165
+ // Parse selector with brackets
166
+ const match = sel.match(/^(@[eaps])(?:\[(.*)\])?$/)
167
+ if (!match) {
168
+ return []
169
+ }
170
+
171
+ const [, selectorType, bracketContent] = match
172
+
173
+ // @s with filters
174
+ if (selectorType === '@s') {
175
+ if (!executor) return []
176
+ const filters = parseFilters(bracketContent || '')
177
+ if (matchesFilters(executor, filters)) {
178
+ return [executor]
179
+ }
180
+ return []
181
+ }
182
+
183
+ // @e/@a with filters
184
+ const filters = parseFilters(bracketContent || '')
185
+ let result = entities.filter(e => matchesFilters(e, filters))
186
+
187
+ // Apply limit
188
+ if (filters.limit !== undefined) {
189
+ result = result.slice(0, filters.limit)
190
+ }
191
+
192
+ return result
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // JSON Component Parsing
197
+ // ---------------------------------------------------------------------------
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // NBT Parsing
201
+ // ---------------------------------------------------------------------------
202
+
203
+ function parseNBT(nbt: string): Record<string, any> {
204
+ // Simple NBT parser for Tags array
205
+ const result: Record<string, any> = {}
206
+
207
+ const tagsMatch = nbt.match(/Tags:\s*\[(.*?)\]/)
208
+ if (tagsMatch) {
209
+ const tagsStr = tagsMatch[1]
210
+ result.Tags = tagsStr
211
+ .split(',')
212
+ .map(s => s.trim().replace(/^["']|["']$/g, ''))
213
+ .filter(s => s.length > 0)
214
+ }
215
+
216
+ return result
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // MCRuntime Class
221
+ // ---------------------------------------------------------------------------
222
+
223
+ export class MCRuntime {
224
+ // Scoreboard state: objective → (player → score)
225
+ scoreboard: Map<string, Map<string, number>> = new Map()
226
+
227
+ // NBT storage: "namespace:path" → JSON value
228
+ storage: Map<string, any> = new Map()
229
+
230
+ // Entities in world
231
+ entities: Entity[] = []
232
+
233
+ // Loaded functions: "ns:name" → lines of mcfunction
234
+ functions: Map<string, string[]> = new Map()
235
+
236
+ // Log of say/tellraw/title output
237
+ chatLog: string[] = []
238
+
239
+ // Simple world state: "x,y,z" -> block id
240
+ world: Map<string, string> = new Map()
241
+
242
+ // Current weather
243
+ weather: string = 'clear'
244
+
245
+ // Current world time
246
+ worldTime: number = 0
247
+
248
+ // Active potion effects by entity id
249
+ effects: Map<string, { effect: string; duration: number; amplifier: number }[]> = new Map()
250
+
251
+ // XP values by player/entity id
252
+ xp: Map<string, number> = new Map()
253
+
254
+ // Tick counter
255
+ tickCount: number = 0
256
+
257
+ // Namespace
258
+ namespace: string
259
+
260
+ // Entity ID counter
261
+ private entityIdCounter = 0
262
+
263
+ // Return value for current function
264
+ private returnValue: number | undefined
265
+
266
+ // Flag to stop function execution (for return)
267
+ private shouldReturn: boolean = false
268
+
269
+ constructor(namespace: string) {
270
+ this.namespace = namespace
271
+ // Initialize default objective
272
+ this.scoreboard.set('rs', new Map())
273
+ }
274
+
275
+ // -------------------------------------------------------------------------
276
+ // Datapack Loading
277
+ // -------------------------------------------------------------------------
278
+
279
+ loadDatapack(dir: string): void {
280
+ const functionsDir = path.join(dir, 'data', this.namespace, 'function')
281
+ if (!fs.existsSync(functionsDir)) return
282
+
283
+ const loadFunctions = (base: string, prefix: string): void => {
284
+ const entries = fs.readdirSync(base, { withFileTypes: true })
285
+ for (const entry of entries) {
286
+ const fullPath = path.join(base, entry.name)
287
+ if (entry.isDirectory()) {
288
+ loadFunctions(fullPath, `${prefix}${entry.name}/`)
289
+ } else if (entry.name.endsWith('.mcfunction')) {
290
+ const fnName = `${prefix}${entry.name.replace('.mcfunction', '')}`
291
+ const content = fs.readFileSync(fullPath, 'utf-8')
292
+ this.loadFunction(`${this.namespace}:${fnName}`, content.split('\n'))
293
+ }
294
+ }
295
+ }
296
+
297
+ loadFunctions(functionsDir, '')
298
+ }
299
+
300
+ loadFunction(name: string, lines: string[]): void {
301
+ // Filter out comments and empty lines, but keep all commands
302
+ const cleaned = lines
303
+ .map(l => l.trim())
304
+ .filter(l => l && !l.startsWith('#'))
305
+ this.functions.set(name, cleaned)
306
+ }
307
+
308
+ // -------------------------------------------------------------------------
309
+ // Lifecycle Methods
310
+ // -------------------------------------------------------------------------
311
+
312
+ load(): void {
313
+ const loadFn = `${this.namespace}:__load`
314
+ if (this.functions.has(loadFn)) {
315
+ this.execFunction(loadFn)
316
+ }
317
+ }
318
+
319
+ tick(): void {
320
+ this.tickCount++
321
+ const tickFn = `${this.namespace}:__tick`
322
+ if (this.functions.has(tickFn)) {
323
+ this.execFunction(tickFn)
324
+ }
325
+ }
326
+
327
+ ticks(n: number): void {
328
+ for (let i = 0; i < n; i++) {
329
+ this.tick()
330
+ }
331
+ }
332
+
333
+ // -------------------------------------------------------------------------
334
+ // Function Execution
335
+ // -------------------------------------------------------------------------
336
+
337
+ execFunction(name: string, executor?: Entity): void {
338
+ const lines = this.functions.get(name)
339
+ if (!lines) {
340
+ // Try with namespace prefix
341
+ const prefixedName = name.includes(':') ? name : `${this.namespace}:${name}`
342
+ const prefixedLines = this.functions.get(prefixedName)
343
+ if (!prefixedLines) return
344
+ this.execFunctionLines(prefixedLines, executor)
345
+ return
346
+ }
347
+ this.execFunctionLines(lines, executor)
348
+ }
349
+
350
+ private execFunctionLines(lines: string[], executor?: Entity): void {
351
+ this.shouldReturn = false
352
+ for (const line of lines) {
353
+ if (this.shouldReturn) break
354
+ this.execCommand(line, executor)
355
+ }
356
+ }
357
+
358
+ // -------------------------------------------------------------------------
359
+ // Command Execution
360
+ // -------------------------------------------------------------------------
361
+
362
+ execCommand(cmd: string, executor?: Entity): boolean {
363
+ cmd = cmd.trim()
364
+ if (!cmd || cmd.startsWith('#')) return true
365
+
366
+ // Parse command
367
+ if (cmd.startsWith('scoreboard ')) {
368
+ return this.execScoreboard(cmd)
369
+ }
370
+ if (cmd.startsWith('execute ')) {
371
+ return this.execExecute(cmd, executor)
372
+ }
373
+ if (cmd.startsWith('function ')) {
374
+ return this.execFunctionCmd(cmd, executor)
375
+ }
376
+ if (cmd.startsWith('data ')) {
377
+ return this.execData(cmd)
378
+ }
379
+ if (cmd.startsWith('tag ')) {
380
+ return this.execTag(cmd, executor)
381
+ }
382
+ if (cmd.startsWith('say ')) {
383
+ return this.execSay(cmd, executor)
384
+ }
385
+ if (cmd.startsWith('tellraw ')) {
386
+ return this.execTellraw(cmd)
387
+ }
388
+ if (cmd.startsWith('title ')) {
389
+ return this.execTitle(cmd)
390
+ }
391
+ if (cmd.startsWith('setblock ')) {
392
+ return this.execSetblock(cmd)
393
+ }
394
+ if (cmd.startsWith('fill ')) {
395
+ return this.execFill(cmd)
396
+ }
397
+ if (cmd.startsWith('tp ')) {
398
+ return this.execTp(cmd, executor)
399
+ }
400
+ if (cmd.startsWith('weather ')) {
401
+ return this.execWeather(cmd)
402
+ }
403
+ if (cmd.startsWith('time ')) {
404
+ return this.execTime(cmd)
405
+ }
406
+ if (cmd.startsWith('kill ')) {
407
+ return this.execKill(cmd, executor)
408
+ }
409
+ if (cmd.startsWith('effect ')) {
410
+ return this.execEffect(cmd, executor)
411
+ }
412
+ if (cmd.startsWith('xp ')) {
413
+ return this.execXp(cmd, executor)
414
+ }
415
+ if (cmd.startsWith('summon ')) {
416
+ return this.execSummon(cmd)
417
+ }
418
+ if (cmd.startsWith('return ')) {
419
+ return this.execReturn(cmd, executor)
420
+ }
421
+ if (cmd === 'return') {
422
+ this.shouldReturn = true
423
+ return true
424
+ }
425
+
426
+ // Unknown command - succeed silently
427
+ return true
428
+ }
429
+
430
+ // -------------------------------------------------------------------------
431
+ // Scoreboard Commands
432
+ // -------------------------------------------------------------------------
433
+
434
+ private execScoreboard(cmd: string): boolean {
435
+ const parts = cmd.split(/\s+/)
436
+
437
+ // scoreboard objectives add <name> <criteria>
438
+ if (parts[1] === 'objectives' && parts[2] === 'add') {
439
+ const name = parts[3]
440
+ if (!this.scoreboard.has(name)) {
441
+ this.scoreboard.set(name, new Map())
442
+ }
443
+ return true
444
+ }
445
+
446
+ // scoreboard players ...
447
+ if (parts[1] === 'players') {
448
+ const action = parts[2]
449
+ const player = parts[3]
450
+ const objective = parts[4]
451
+
452
+ switch (action) {
453
+ case 'set': {
454
+ const value = parseInt(parts[5], 10)
455
+ this.setScore(player, objective, value)
456
+ return true
457
+ }
458
+ case 'add': {
459
+ const delta = parseInt(parts[5], 10)
460
+ this.addScore(player, objective, delta)
461
+ return true
462
+ }
463
+ case 'remove': {
464
+ const delta = parseInt(parts[5], 10)
465
+ this.addScore(player, objective, -delta)
466
+ return true
467
+ }
468
+ case 'get': {
469
+ this.returnValue = this.getScore(player, objective)
470
+ return true
471
+ }
472
+ case 'reset': {
473
+ const obj = this.scoreboard.get(objective)
474
+ if (obj) obj.delete(player)
475
+ return true
476
+ }
477
+ case 'enable': {
478
+ // No-op for trigger enabling
479
+ return true
480
+ }
481
+ case 'operation': {
482
+ // scoreboard players operation <target> <targetObj> <op> <source> <sourceObj>
483
+ const targetObj = objective
484
+ const op = parts[5]
485
+ const source = parts[6]
486
+ const sourceObj = parts[7]
487
+
488
+ const targetVal = this.getScore(player, targetObj)
489
+ const sourceVal = this.getScore(source, sourceObj)
490
+
491
+ let result: number
492
+ switch (op) {
493
+ case '=':
494
+ result = sourceVal
495
+ break
496
+ case '+=':
497
+ result = targetVal + sourceVal
498
+ break
499
+ case '-=':
500
+ result = targetVal - sourceVal
501
+ break
502
+ case '*=':
503
+ result = targetVal * sourceVal
504
+ break
505
+ case '/=':
506
+ result = Math.trunc(targetVal / sourceVal)
507
+ break
508
+ case '%=':
509
+ result = targetVal % sourceVal // Java modulo: sign follows dividend
510
+ break
511
+ case '<':
512
+ result = Math.min(targetVal, sourceVal)
513
+ break
514
+ case '>':
515
+ result = Math.max(targetVal, sourceVal)
516
+ break
517
+ case '><':
518
+ // Swap
519
+ this.setScore(player, targetObj, sourceVal)
520
+ this.setScore(source, sourceObj, targetVal)
521
+ return true
522
+ default:
523
+ return false
524
+ }
525
+ this.setScore(player, targetObj, result)
526
+ return true
527
+ }
528
+ }
529
+ }
530
+
531
+ return false
532
+ }
533
+
534
+ // -------------------------------------------------------------------------
535
+ // Execute Commands
536
+ // -------------------------------------------------------------------------
537
+
538
+ private execExecute(cmd: string, executor?: Entity): boolean {
539
+ // Remove 'execute ' prefix
540
+ let rest = cmd.slice(8)
541
+
542
+ // Track execute state
543
+ let currentExecutor = executor
544
+ let condition: boolean = true
545
+ let storeTarget: { player: string; objective: string; type: 'result' | 'success' } | null = null
546
+
547
+ while (rest.length > 0) {
548
+ rest = rest.trimStart()
549
+
550
+ // Handle 'run' - execute the final command
551
+ if (rest.startsWith('run ')) {
552
+ if (!condition) return false
553
+ const innerCmd = rest.slice(4)
554
+ const result = this.execCommand(innerCmd, currentExecutor)
555
+
556
+ if (storeTarget) {
557
+ const value = storeTarget.type === 'result'
558
+ ? (this.returnValue ?? (result ? 1 : 0))
559
+ : (result ? 1 : 0)
560
+ this.setScore(storeTarget.player, storeTarget.objective, value)
561
+ }
562
+
563
+ return result
564
+ }
565
+
566
+ // Handle 'as <selector>'
567
+ if (rest.startsWith('as ')) {
568
+ rest = rest.slice(3)
569
+ const { selector, remaining } = this.parseNextSelector(rest)
570
+ rest = remaining
571
+
572
+ const entities = parseSelector(selector, this.entities, currentExecutor)
573
+ if (entities.length === 0) return false
574
+
575
+ // For multiple entities, execute as each
576
+ if (entities.length > 1) {
577
+ let success = false
578
+ for (const entity of entities) {
579
+ const result = this.execCommand('execute ' + rest, entity)
580
+ success = success || result
581
+ }
582
+ return success
583
+ }
584
+
585
+ currentExecutor = entities[0]
586
+ continue
587
+ }
588
+
589
+ // Handle 'at <selector>' - no-op for position, just continue
590
+ if (rest.startsWith('at ')) {
591
+ rest = rest.slice(3)
592
+ const { remaining } = this.parseNextSelector(rest)
593
+ rest = remaining
594
+ continue
595
+ }
596
+
597
+ // Handle 'if score <player> <obj> matches <range>'
598
+ if (rest.startsWith('if score ')) {
599
+ rest = rest.slice(9)
600
+ const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
601
+ if (scoreParts) {
602
+ const [, player, obj, rangeStr, remaining] = scoreParts
603
+ const range = parseRange(rangeStr)
604
+ const score = this.getScore(player, obj)
605
+ condition = condition && matchesRange(score, range)
606
+ rest = remaining.trim()
607
+ continue
608
+ }
609
+
610
+ // if score <p1> <o1> <op> <p2> <o2>
611
+ const compareMatch = rest.match(/^(\S+)\s+(\S+)\s+([<>=]+)\s+(\S+)\s+(\S+)(.*)$/)
612
+ if (compareMatch) {
613
+ const [, p1, o1, op, p2, o2, remaining] = compareMatch
614
+ const v1 = this.getScore(p1, o1)
615
+ const v2 = this.getScore(p2, o2)
616
+ let matches = false
617
+ switch (op) {
618
+ case '=': matches = v1 === v2; break
619
+ case '<': matches = v1 < v2; break
620
+ case '<=': matches = v1 <= v2; break
621
+ case '>': matches = v1 > v2; break
622
+ case '>=': matches = v1 >= v2; break
623
+ }
624
+ condition = condition && matches
625
+ rest = remaining.trim()
626
+ continue
627
+ }
628
+ }
629
+
630
+ // Handle 'unless score ...'
631
+ if (rest.startsWith('unless score ')) {
632
+ rest = rest.slice(13)
633
+ const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
634
+ if (scoreParts) {
635
+ const [, player, obj, rangeStr, remaining] = scoreParts
636
+ const range = parseRange(rangeStr)
637
+ const score = this.getScore(player, obj)
638
+ condition = condition && !matchesRange(score, range)
639
+ rest = remaining.trim()
640
+ continue
641
+ }
642
+ }
643
+
644
+ // Handle 'if entity <selector>'
645
+ if (rest.startsWith('if entity ')) {
646
+ rest = rest.slice(10)
647
+ const { selector, remaining } = this.parseNextSelector(rest)
648
+ rest = remaining
649
+ const entities = parseSelector(selector, this.entities, currentExecutor)
650
+ condition = condition && entities.length > 0
651
+ continue
652
+ }
653
+
654
+ // Handle 'unless entity <selector>'
655
+ if (rest.startsWith('unless entity ')) {
656
+ rest = rest.slice(14)
657
+ const { selector, remaining } = this.parseNextSelector(rest)
658
+ rest = remaining
659
+ const entities = parseSelector(selector, this.entities, currentExecutor)
660
+ condition = condition && entities.length === 0
661
+ continue
662
+ }
663
+
664
+ // Handle 'store result score <player> <obj>'
665
+ if (rest.startsWith('store result score ')) {
666
+ rest = rest.slice(19)
667
+ const storeParts = rest.match(/^(\S+)\s+(\S+)(.*)$/)
668
+ if (storeParts) {
669
+ const [, player, obj, remaining] = storeParts
670
+ storeTarget = { player, objective: obj, type: 'result' }
671
+ rest = remaining.trim()
672
+ continue
673
+ }
674
+ }
675
+
676
+ // Handle 'store success score <player> <obj>'
677
+ if (rest.startsWith('store success score ')) {
678
+ rest = rest.slice(20)
679
+ const storeParts = rest.match(/^(\S+)\s+(\S+)(.*)$/)
680
+ if (storeParts) {
681
+ const [, player, obj, remaining] = storeParts
682
+ storeTarget = { player, objective: obj, type: 'success' }
683
+ rest = remaining.trim()
684
+ continue
685
+ }
686
+ }
687
+
688
+ // Unknown subcommand - skip to next space or 'run'
689
+ const nextSpace = rest.indexOf(' ')
690
+ if (nextSpace === -1) break
691
+ rest = rest.slice(nextSpace + 1)
692
+ }
693
+
694
+ if (storeTarget) {
695
+ const value = storeTarget.type === 'result'
696
+ ? (this.returnValue ?? (condition ? 1 : 0))
697
+ : (condition ? 1 : 0)
698
+ this.setScore(storeTarget.player, storeTarget.objective, value)
699
+ }
700
+
701
+ return condition
702
+ }
703
+
704
+ private parseNextSelector(input: string): { selector: string; remaining: string } {
705
+ input = input.trimStart()
706
+ const match = input.match(/^(@[eaps])(\[[^\]]*\])?/)
707
+ if (match) {
708
+ const selector = match[0]
709
+ return { selector, remaining: input.slice(selector.length).trim() }
710
+ }
711
+ // Non-selector target
712
+ const spaceIdx = input.indexOf(' ')
713
+ if (spaceIdx === -1) {
714
+ return { selector: input, remaining: '' }
715
+ }
716
+ return { selector: input.slice(0, spaceIdx), remaining: input.slice(spaceIdx + 1) }
717
+ }
718
+
719
+ // -------------------------------------------------------------------------
720
+ // Function Command
721
+ // -------------------------------------------------------------------------
722
+
723
+ private execFunctionCmd(cmd: string, executor?: Entity): boolean {
724
+ const fnName = cmd.slice(9).trim() // remove 'function '
725
+ const outerShouldReturn = this.shouldReturn
726
+ this.execFunction(fnName, executor)
727
+ this.shouldReturn = outerShouldReturn
728
+ return true
729
+ }
730
+
731
+ // -------------------------------------------------------------------------
732
+ // Data Commands
733
+ // -------------------------------------------------------------------------
734
+
735
+ private execData(cmd: string): boolean {
736
+ // data modify storage <ns:path> <field> set value <val>
737
+ const setMatch = cmd.match(/^data modify storage (\S+) (\S+) set value (.+)$/)
738
+ if (setMatch) {
739
+ const [, storagePath, field, valueStr] = setMatch
740
+ const value = this.parseDataValue(valueStr)
741
+ this.setStorageField(storagePath, field, value)
742
+ return true
743
+ }
744
+
745
+ // data modify storage <ns:path> <field> append value <val>
746
+ const appendMatch = cmd.match(/^data modify storage (\S+) (\S+) append value (.+)$/)
747
+ if (appendMatch) {
748
+ const [, storagePath, field, valueStr] = appendMatch
749
+ const value = this.parseDataValue(valueStr)
750
+ const current = this.getStorageField(storagePath, field) ?? []
751
+ if (Array.isArray(current)) {
752
+ current.push(value)
753
+ this.setStorageField(storagePath, field, current)
754
+ }
755
+ return true
756
+ }
757
+
758
+ // data get storage <ns:path> <field>
759
+ const getMatch = cmd.match(/^data get storage (\S+) (\S+)$/)
760
+ if (getMatch) {
761
+ const [, storagePath, field] = getMatch
762
+ const value = this.getStorageField(storagePath, field)
763
+ if (typeof value === 'number') {
764
+ this.returnValue = value
765
+ } else if (Array.isArray(value)) {
766
+ this.returnValue = value.length
767
+ } else {
768
+ this.returnValue = value ? 1 : 0
769
+ }
770
+ return true
771
+ }
772
+
773
+ // data modify storage <ns:path> <field> set from storage <src> <srcpath>
774
+ const copyMatch = cmd.match(/^data modify storage (\S+) (\S+) set from storage (\S+) (\S+)$/)
775
+ if (copyMatch) {
776
+ const [, dstPath, dstField, srcPath, srcField] = copyMatch
777
+ const value = this.getStorageField(srcPath, srcField)
778
+ this.setStorageField(dstPath, dstField, value)
779
+ return true
780
+ }
781
+
782
+ // data remove storage <ns:path> <field>
783
+ const removeMatch = cmd.match(/^data remove storage (\S+) (\S+)$/)
784
+ if (removeMatch) {
785
+ const [, storagePath, field] = removeMatch
786
+ return this.removeStorageField(storagePath, field)
787
+ }
788
+
789
+ return false
790
+ }
791
+
792
+ private parseDataValue(str: string): any {
793
+ str = str.trim()
794
+ // Try JSON parse
795
+ try {
796
+ return JSON.parse(str)
797
+ } catch {
798
+ // Try numeric
799
+ const num = parseFloat(str)
800
+ if (!isNaN(num)) return num
801
+ // Return as string
802
+ return str
803
+ }
804
+ }
805
+
806
+ private getStorageField(storagePath: string, field: string): any {
807
+ const data = this.storage.get(storagePath) ?? {}
808
+ const segments = this.parseStoragePath(field)
809
+ let current: any = data
810
+ for (const segment of segments) {
811
+ if (typeof segment === 'number') {
812
+ if (!Array.isArray(current)) return undefined
813
+ const index = segment < 0 ? current.length + segment : segment
814
+ current = current[index]
815
+ continue
816
+ }
817
+ if (current == null || typeof current !== 'object') return undefined
818
+ current = current[segment]
819
+ }
820
+ return current
821
+ }
822
+
823
+ private setStorageField(storagePath: string, field: string, value: any): void {
824
+ let data = this.storage.get(storagePath)
825
+ if (!data) {
826
+ data = {}
827
+ this.storage.set(storagePath, data)
828
+ }
829
+ const segments = this.parseStoragePath(field)
830
+ let current: any = data
831
+ for (let i = 0; i < segments.length - 1; i++) {
832
+ const segment = segments[i]
833
+ const next = segments[i + 1]
834
+ if (typeof segment === 'number') {
835
+ if (!Array.isArray(current)) return
836
+ const index = segment < 0 ? current.length + segment : segment
837
+ if (current[index] === undefined) {
838
+ current[index] = typeof next === 'number' ? [] : {}
839
+ }
840
+ current = current[index]
841
+ continue
842
+ }
843
+ if (!(segment in current)) {
844
+ current[segment] = typeof next === 'number' ? [] : {}
845
+ }
846
+ current = current[segment]
847
+ }
848
+
849
+ const last = segments[segments.length - 1]
850
+ if (typeof last === 'number') {
851
+ if (!Array.isArray(current)) return
852
+ const index = last < 0 ? current.length + last : last
853
+ current[index] = value
854
+ return
855
+ }
856
+ current[last] = value
857
+ }
858
+
859
+ private removeStorageField(storagePath: string, field: string): boolean {
860
+ const data = this.storage.get(storagePath)
861
+ if (!data) return false
862
+
863
+ const segments = this.parseStoragePath(field)
864
+ let current: any = data
865
+ for (let i = 0; i < segments.length - 1; i++) {
866
+ const segment = segments[i]
867
+ if (typeof segment === 'number') {
868
+ if (!Array.isArray(current)) return false
869
+ const index = segment < 0 ? current.length + segment : segment
870
+ current = current[index]
871
+ } else {
872
+ current = current?.[segment]
873
+ }
874
+ if (current === undefined) return false
875
+ }
876
+
877
+ const last = segments[segments.length - 1]
878
+ if (typeof last === 'number') {
879
+ if (!Array.isArray(current)) return false
880
+ const index = last < 0 ? current.length + last : last
881
+ if (index < 0 || index >= current.length) return false
882
+ current.splice(index, 1)
883
+ return true
884
+ }
885
+
886
+ if (current == null || typeof current !== 'object' || !(last in current)) return false
887
+ delete current[last]
888
+ return true
889
+ }
890
+
891
+ private parseStoragePath(field: string): Array<string | number> {
892
+ return field
893
+ .split('.')
894
+ .flatMap(part => {
895
+ const segments: Array<string | number> = []
896
+ const regex = /([^\[\]]+)|\[(-?\d+)\]/g
897
+ for (const match of part.matchAll(regex)) {
898
+ if (match[1]) segments.push(match[1])
899
+ if (match[2]) segments.push(parseInt(match[2], 10))
900
+ }
901
+ return segments
902
+ })
903
+ }
904
+
905
+ // -------------------------------------------------------------------------
906
+ // Tag Commands
907
+ // -------------------------------------------------------------------------
908
+
909
+ private execTag(cmd: string, executor?: Entity): boolean {
910
+ // tag <selector> add <name>
911
+ const addMatch = cmd.match(/^tag (\S+) add (\S+)$/)
912
+ if (addMatch) {
913
+ const [, selStr, tagName] = addMatch
914
+ const entities = selStr === '@s' && executor
915
+ ? [executor]
916
+ : parseSelector(selStr, this.entities, executor)
917
+ for (const entity of entities) {
918
+ entity.tags.add(tagName)
919
+ }
920
+ return entities.length > 0
921
+ }
922
+
923
+ // tag <selector> remove <name>
924
+ const removeMatch = cmd.match(/^tag (\S+) remove (\S+)$/)
925
+ if (removeMatch) {
926
+ const [, selStr, tagName] = removeMatch
927
+ const entities = selStr === '@s' && executor
928
+ ? [executor]
929
+ : parseSelector(selStr, this.entities, executor)
930
+ for (const entity of entities) {
931
+ entity.tags.delete(tagName)
932
+ }
933
+ return entities.length > 0
934
+ }
935
+
936
+ return false
937
+ }
938
+
939
+ // -------------------------------------------------------------------------
940
+ // Say/Tellraw/Title Commands
941
+ // -------------------------------------------------------------------------
942
+
943
+ private execSay(cmd: string, executor?: Entity): boolean {
944
+ const message = cmd.slice(4)
945
+ this.chatLog.push(`[${executor?.id ?? 'Server'}] ${message}`)
946
+ return true
947
+ }
948
+
949
+ private execTellraw(cmd: string): boolean {
950
+ // tellraw <selector> <json>
951
+ const match = cmd.match(/^tellraw \S+ (.+)$/)
952
+ if (match) {
953
+ const jsonStr = match[1]
954
+ const text = this.extractJsonText(jsonStr)
955
+ this.chatLog.push(text)
956
+ return true
957
+ }
958
+ return false
959
+ }
960
+
961
+ private execTitle(cmd: string): boolean {
962
+ // title <selector> <kind> <json>
963
+ const match = cmd.match(/^title \S+ (actionbar|title|subtitle) (.+)$/)
964
+ if (match) {
965
+ const [, kind, jsonStr] = match
966
+ const text = this.extractJsonText(jsonStr)
967
+ this.chatLog.push(`[${kind.toUpperCase()}] ${text}`)
968
+ return true
969
+ }
970
+ return false
971
+ }
972
+
973
+ private extractJsonText(json: any): string {
974
+ if (typeof json === 'string') {
975
+ try {
976
+ json = JSON.parse(json)
977
+ } catch {
978
+ return json
979
+ }
980
+ }
981
+
982
+ if (typeof json === 'string') return json
983
+ if (Array.isArray(json)) {
984
+ return json.map(part => this.extractJsonText(part)).join('')
985
+ }
986
+ if (typeof json === 'object' && json !== null) {
987
+ if ('text' in json) return String(json.text)
988
+ if ('score' in json && typeof json.score === 'object' && json.score !== null) {
989
+ const name = 'name' in json.score ? String(json.score.name) : ''
990
+ const objective = 'objective' in json.score ? String(json.score.objective) : 'rs'
991
+ return String(this.getScore(name, objective))
992
+ }
993
+ if ('extra' in json && Array.isArray(json.extra)) {
994
+ return json.extra.map((part: any) => this.extractJsonText(part)).join('')
995
+ }
996
+ }
997
+ return ''
998
+ }
999
+
1000
+ // -------------------------------------------------------------------------
1001
+ // World Commands
1002
+ // -------------------------------------------------------------------------
1003
+
1004
+ private execSetblock(cmd: string): boolean {
1005
+ const match = cmd.match(/^setblock (\S+) (\S+) (\S+) (\S+)$/)
1006
+ if (!match) return false
1007
+
1008
+ const [, x, y, z, block] = match
1009
+ const key = this.positionKey(x, y, z)
1010
+ if (!key) return false
1011
+ this.world.set(key, block)
1012
+ return true
1013
+ }
1014
+
1015
+ private execFill(cmd: string): boolean {
1016
+ const match = cmd.match(/^fill (\S+) (\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$/)
1017
+ if (!match) return false
1018
+
1019
+ const [, x1, y1, z1, x2, y2, z2, block] = match
1020
+ const start = this.parseAbsolutePosition(x1, y1, z1)
1021
+ const end = this.parseAbsolutePosition(x2, y2, z2)
1022
+ if (!start || !end) return false
1023
+
1024
+ const [minX, maxX] = [Math.min(start.x, end.x), Math.max(start.x, end.x)]
1025
+ const [minY, maxY] = [Math.min(start.y, end.y), Math.max(start.y, end.y)]
1026
+ const [minZ, maxZ] = [Math.min(start.z, end.z), Math.max(start.z, end.z)]
1027
+
1028
+ for (let x = minX; x <= maxX; x++) {
1029
+ for (let y = minY; y <= maxY; y++) {
1030
+ for (let z = minZ; z <= maxZ; z++) {
1031
+ this.world.set(`${x},${y},${z}`, block)
1032
+ }
1033
+ }
1034
+ }
1035
+ return true
1036
+ }
1037
+
1038
+ private execTp(cmd: string, executor?: Entity): boolean {
1039
+ const selfCoordsMatch = cmd.match(/^tp (\S+) (\S+) (\S+)$/)
1040
+ if (selfCoordsMatch && executor) {
1041
+ const [, x, y, z] = selfCoordsMatch
1042
+ const next = this.resolvePosition(executor.position ?? { x: 0, y: 0, z: 0 }, x, y, z)
1043
+ if (!next) return false
1044
+ executor.position = next
1045
+ return true
1046
+ }
1047
+
1048
+ const coordsMatch = cmd.match(/^tp (\S+) (\S+) (\S+) (\S+)$/)
1049
+ if (coordsMatch) {
1050
+ const [, selStr, x, y, z] = coordsMatch
1051
+ const entities = selStr === '@s' && executor
1052
+ ? [executor]
1053
+ : parseSelector(selStr, this.entities, executor)
1054
+ for (const entity of entities) {
1055
+ const next = this.resolvePosition(entity.position ?? { x: 0, y: 0, z: 0 }, x, y, z)
1056
+ if (next) {
1057
+ entity.position = next
1058
+ }
1059
+ }
1060
+ return entities.length > 0
1061
+ }
1062
+
1063
+ const entityMatch = cmd.match(/^tp (\S+) (\S+)$/)
1064
+ if (entityMatch) {
1065
+ const [, selStr, targetStr] = entityMatch
1066
+ const entities = selStr === '@s' && executor
1067
+ ? [executor]
1068
+ : parseSelector(selStr, this.entities, executor)
1069
+ const target = targetStr === '@s' && executor
1070
+ ? executor
1071
+ : parseSelector(targetStr, this.entities, executor)[0]
1072
+ if (!target?.position) return false
1073
+ for (const entity of entities) {
1074
+ entity.position = { ...target.position }
1075
+ }
1076
+ return entities.length > 0
1077
+ }
1078
+
1079
+ return false
1080
+ }
1081
+
1082
+ private execWeather(cmd: string): boolean {
1083
+ const match = cmd.match(/^weather (\S+)$/)
1084
+ if (!match) return false
1085
+ this.weather = match[1]
1086
+ return true
1087
+ }
1088
+
1089
+ private execTime(cmd: string): boolean {
1090
+ const match = cmd.match(/^time (set|add) (\S+)$/)
1091
+ if (!match) return false
1092
+
1093
+ const [, action, valueStr] = match
1094
+ const value = this.parseTimeValue(valueStr)
1095
+ if (value === null) return false
1096
+
1097
+ if (action === 'set') {
1098
+ this.worldTime = value
1099
+ } else {
1100
+ this.worldTime += value
1101
+ }
1102
+ return true
1103
+ }
1104
+
1105
+ // -------------------------------------------------------------------------
1106
+ // Kill Command
1107
+ // -------------------------------------------------------------------------
1108
+
1109
+ private execKill(cmd: string, executor?: Entity): boolean {
1110
+ const selStr = cmd.slice(5).trim()
1111
+
1112
+ if (selStr === '@s' && executor) {
1113
+ this.entities = this.entities.filter(e => e !== executor)
1114
+ return true
1115
+ }
1116
+
1117
+ const entities = parseSelector(selStr, this.entities, executor)
1118
+ for (const entity of entities) {
1119
+ this.entities = this.entities.filter(e => e !== entity)
1120
+ }
1121
+ return entities.length > 0
1122
+ }
1123
+
1124
+ // -------------------------------------------------------------------------
1125
+ // Effect / XP Commands
1126
+ // -------------------------------------------------------------------------
1127
+
1128
+ private execEffect(cmd: string, executor?: Entity): boolean {
1129
+ const match = cmd.match(/^effect give (\S+) (\S+)(?: (\S+))?(?: (\S+))?(?: \S+)?$/)
1130
+ if (!match) return false
1131
+
1132
+ const [, selStr, effect, durationStr, amplifierStr] = match
1133
+ const entities = selStr === '@s' && executor
1134
+ ? [executor]
1135
+ : parseSelector(selStr, this.entities, executor)
1136
+
1137
+ const duration = durationStr ? parseInt(durationStr, 10) : 30
1138
+ const amplifier = amplifierStr ? parseInt(amplifierStr, 10) : 0
1139
+ for (const entity of entities) {
1140
+ const current = this.effects.get(entity.id) ?? []
1141
+ current.push({ effect, duration: isNaN(duration) ? 30 : duration, amplifier: isNaN(amplifier) ? 0 : amplifier })
1142
+ this.effects.set(entity.id, current)
1143
+ }
1144
+ return entities.length > 0
1145
+ }
1146
+
1147
+ private execXp(cmd: string, executor?: Entity): boolean {
1148
+ const match = cmd.match(/^xp (add|set) (\S+) (-?\d+)(?: (\S+))?$/)
1149
+ if (!match) return false
1150
+
1151
+ const [, action, target, amountStr] = match
1152
+ const amount = parseInt(amountStr, 10)
1153
+ const keys = this.resolveTargetKeys(target, executor)
1154
+ if (keys.length === 0) return false
1155
+
1156
+ for (const key of keys) {
1157
+ const current = this.xp.get(key) ?? 0
1158
+ this.xp.set(key, action === 'set' ? amount : current + amount)
1159
+ }
1160
+ return true
1161
+ }
1162
+
1163
+ // -------------------------------------------------------------------------
1164
+ // Summon Command
1165
+ // -------------------------------------------------------------------------
1166
+
1167
+ private execSummon(cmd: string): boolean {
1168
+ // summon minecraft:armor_stand <x> <y> <z> {Tags:["tag1","tag2"]}
1169
+ const match = cmd.match(/^summon (\S+) (\S+) (\S+) (\S+) ({.+})$/)
1170
+ if (match) {
1171
+ const [, type, x, y, z, nbtStr] = match
1172
+ const nbt = parseNBT(nbtStr)
1173
+ const position = this.parseAbsolutePosition(x, y, z) ?? { x: 0, y: 0, z: 0 }
1174
+ this.spawnEntity(nbt.Tags || [], type, position)
1175
+ return true
1176
+ }
1177
+
1178
+ // Simple summon without NBT
1179
+ const simpleMatch = cmd.match(/^summon (\S+)(?: (\S+) (\S+) (\S+))?$/)
1180
+ if (simpleMatch) {
1181
+ const [, type, x, y, z] = simpleMatch
1182
+ const position = x && y && z
1183
+ ? (this.parseAbsolutePosition(x, y, z) ?? { x: 0, y: 0, z: 0 })
1184
+ : { x: 0, y: 0, z: 0 }
1185
+ this.spawnEntity([], type, position)
1186
+ return true
1187
+ }
1188
+
1189
+ return false
1190
+ }
1191
+
1192
+ // -------------------------------------------------------------------------
1193
+ // Return Command
1194
+ // -------------------------------------------------------------------------
1195
+
1196
+ private execReturn(cmd: string, executor?: Entity): boolean {
1197
+ const rest = cmd.slice(7).trim()
1198
+
1199
+ // return run <cmd>
1200
+ if (rest.startsWith('run ')) {
1201
+ const innerCmd = rest.slice(4)
1202
+ this.execCommand(innerCmd, executor)
1203
+ this.shouldReturn = true
1204
+ return true
1205
+ }
1206
+
1207
+ // return <value>
1208
+ const value = parseInt(rest, 10)
1209
+ if (!isNaN(value)) {
1210
+ this.returnValue = value
1211
+ this.shouldReturn = true
1212
+ return true
1213
+ }
1214
+
1215
+ return false
1216
+ }
1217
+
1218
+ // -------------------------------------------------------------------------
1219
+ // Scoreboard Helpers
1220
+ // -------------------------------------------------------------------------
1221
+
1222
+ getScore(player: string, objective: string): number {
1223
+ const obj = this.scoreboard.get(objective)
1224
+ if (!obj) return 0
1225
+ return obj.get(player) ?? 0
1226
+ }
1227
+
1228
+ setScore(player: string, objective: string, value: number): void {
1229
+ let obj = this.scoreboard.get(objective)
1230
+ if (!obj) {
1231
+ obj = new Map()
1232
+ this.scoreboard.set(objective, obj)
1233
+ }
1234
+ obj.set(player, value)
1235
+ }
1236
+
1237
+ addScore(player: string, objective: string, delta: number): void {
1238
+ const current = this.getScore(player, objective)
1239
+ this.setScore(player, objective, current + delta)
1240
+ }
1241
+
1242
+ // -------------------------------------------------------------------------
1243
+ // Storage Helpers
1244
+ // -------------------------------------------------------------------------
1245
+
1246
+ getStorage(path: string): any {
1247
+ // "ns:path.field" → parse namespace and nested fields
1248
+ const colonIdx = path.indexOf(':')
1249
+ if (colonIdx === -1) return this.storage.get(path)
1250
+
1251
+ const nsPath = path.slice(0, colonIdx + 1) + path.slice(colonIdx + 1).split('.')[0]
1252
+ const field = path.slice(colonIdx + 1).includes('.')
1253
+ ? path.slice(path.indexOf('.', colonIdx) + 1)
1254
+ : undefined
1255
+
1256
+ if (!field) return this.storage.get(nsPath)
1257
+ return this.getStorageField(nsPath, field)
1258
+ }
1259
+
1260
+ setStorage(path: string, value: any): void {
1261
+ const colonIdx = path.indexOf(':')
1262
+ if (colonIdx === -1) {
1263
+ this.storage.set(path, value)
1264
+ return
1265
+ }
1266
+
1267
+ const basePath = path.slice(0, colonIdx + 1) + path.slice(colonIdx + 1).split('.')[0]
1268
+ const field = path.slice(colonIdx + 1).includes('.')
1269
+ ? path.slice(path.indexOf('.', colonIdx) + 1)
1270
+ : undefined
1271
+
1272
+ if (!field) {
1273
+ this.storage.set(basePath, value)
1274
+ return
1275
+ }
1276
+
1277
+ this.setStorageField(basePath, field, value)
1278
+ }
1279
+
1280
+ // -------------------------------------------------------------------------
1281
+ // Entity Helpers
1282
+ // -------------------------------------------------------------------------
1283
+
1284
+ spawnEntity(tags: string[], type: string = 'minecraft:armor_stand', position: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }): Entity {
1285
+ const id = `entity_${this.entityIdCounter++}`
1286
+ const entity: Entity = {
1287
+ id,
1288
+ tags: new Set(tags),
1289
+ scores: new Map(),
1290
+ selector: `@e[tag=${tags[0] ?? id},limit=1]`,
1291
+ type,
1292
+ position,
1293
+ }
1294
+ this.entities.push(entity)
1295
+ return entity
1296
+ }
1297
+
1298
+ killEntity(tag: string): void {
1299
+ this.entities = this.entities.filter(e => !e.tags.has(tag))
1300
+ }
1301
+
1302
+ getEntities(selector: string): Entity[] {
1303
+ return parseSelector(selector, this.entities)
1304
+ }
1305
+
1306
+ private positionKey(x: string, y: string, z: string): string | null {
1307
+ const pos = this.parseAbsolutePosition(x, y, z)
1308
+ return pos ? `${pos.x},${pos.y},${pos.z}` : null
1309
+ }
1310
+
1311
+ private parseAbsolutePosition(x: string, y: string, z: string): { x: number; y: number; z: number } | null {
1312
+ const coords = [x, y, z].map(coord => {
1313
+ if (coord.startsWith('~') || coord.startsWith('^')) {
1314
+ const offset = coord.slice(1)
1315
+ return offset === '' ? 0 : parseInt(offset, 10)
1316
+ }
1317
+ return parseInt(coord, 10)
1318
+ })
1319
+ if (coords.some(Number.isNaN)) return null
1320
+ return { x: coords[0], y: coords[1], z: coords[2] }
1321
+ }
1322
+
1323
+ private resolvePosition(base: { x: number; y: number; z: number }, x: string, y: string, z: string): { x: number; y: number; z: number } | null {
1324
+ const values = [x, y, z].map((coord, index) => {
1325
+ if (coord.startsWith('~') || coord.startsWith('^')) {
1326
+ const offset = coord.slice(1)
1327
+ const delta = offset === '' ? 0 : parseInt(offset, 10)
1328
+ return [base.x, base.y, base.z][index] + delta
1329
+ }
1330
+ return parseInt(coord, 10)
1331
+ })
1332
+ if (values.some(Number.isNaN)) return null
1333
+ return { x: values[0], y: values[1], z: values[2] }
1334
+ }
1335
+
1336
+ private parseTimeValue(value: string): number | null {
1337
+ if (/^-?\d+$/.test(value)) {
1338
+ return parseInt(value, 10)
1339
+ }
1340
+
1341
+ const aliases: Record<string, number> = {
1342
+ day: 1000,
1343
+ noon: 6000,
1344
+ night: 13000,
1345
+ midnight: 18000,
1346
+ sunrise: 23000,
1347
+ }
1348
+ return aliases[value] ?? null
1349
+ }
1350
+
1351
+ private resolveTargetKeys(target: string, executor?: Entity): string[] {
1352
+ if (target.startsWith('@')) {
1353
+ const entities = target === '@s' && executor
1354
+ ? [executor]
1355
+ : parseSelector(target, this.entities, executor)
1356
+ return entities.map(entity => entity.id)
1357
+ }
1358
+ return [target]
1359
+ }
1360
+
1361
+ // -------------------------------------------------------------------------
1362
+ // Output Helpers
1363
+ // -------------------------------------------------------------------------
1364
+
1365
+ getLastSaid(): string {
1366
+ return this.chatLog[this.chatLog.length - 1] ?? ''
1367
+ }
1368
+
1369
+ getChatLog(): string[] {
1370
+ return [...this.chatLog]
1371
+ }
1372
+
1373
+ // -------------------------------------------------------------------------
1374
+ // Convenience: Compile and Load
1375
+ // -------------------------------------------------------------------------
1376
+
1377
+ compileAndLoad(source: string): void {
1378
+ const result = rsCompile(source, { namespace: this.namespace })
1379
+ if (!result.success || !result.files) {
1380
+ throw new Error('Compilation failed')
1381
+ }
1382
+
1383
+ // Load all .mcfunction files
1384
+ for (const file of result.files) {
1385
+ if (file.path.endsWith('.mcfunction')) {
1386
+ // Extract function name from path
1387
+ // e.g., "data/test/function/increment.mcfunction" → "test:increment"
1388
+ const match = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
1389
+ if (match) {
1390
+ const [, ns, fnPath] = match
1391
+ const fnName = `${ns}:${fnPath.replace(/\//g, '/')}`
1392
+ this.loadFunction(fnName, file.content.split('\n'))
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ // Run load function
1398
+ this.load()
1399
+ }
1400
+ }
1401
+
1402
+ // Re-export for convenience
1403
+ export { parseRange, matchesRange, parseSelector }