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,409 @@
1
+ /**
2
+ * RedScript MC Integration Tests
3
+ *
4
+ * Tests compiled datapacks against a real Paper 1.21.4 server.
5
+ *
6
+ * Prerequisites:
7
+ * - Paper server running with TestHarnessPlugin on port 25561
8
+ * - MC_SERVER_DIR env var pointing to server directory
9
+ *
10
+ * Run: MC_SERVER_DIR=~/mc-test-server npx jest mc-integration --testTimeout=120000
11
+ */
12
+
13
+ import * as fs from 'fs'
14
+ import * as path from 'path'
15
+ import { compile } from '../compile'
16
+ import { MCTestClient } from '../mc-test/client'
17
+
18
+ const MC_HOST = process.env.MC_HOST ?? 'localhost'
19
+ const MC_PORT = parseInt(process.env.MC_PORT ?? '25561')
20
+ const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME!, 'mc-test-server')
21
+ const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test')
22
+
23
+ let serverOnline = false
24
+ let mc: MCTestClient
25
+
26
+ /** Write compiled RedScript source into the shared test datapack directory.
27
+ * Merges minecraft tag files (tick.json / load.json) instead of overwriting. */
28
+ function writeFixture(source: string, namespace: string): void {
29
+ fs.mkdirSync(DATAPACK_DIR, { recursive: true })
30
+ // Write pack.mcmeta once
31
+ if (!fs.existsSync(path.join(DATAPACK_DIR, 'pack.mcmeta'))) {
32
+ fs.writeFileSync(path.join(DATAPACK_DIR, 'pack.mcmeta'), JSON.stringify({
33
+ pack: { pack_format: 48, description: 'RedScript integration tests' }
34
+ }))
35
+ }
36
+
37
+ const result = compile(source, { namespace })
38
+ if (result.error) throw new Error(`Compile error in ${namespace}: ${result.error}`)
39
+
40
+ for (const file of result.files ?? []) {
41
+ if (file.path === 'pack.mcmeta') continue
42
+ const filePath = path.join(DATAPACK_DIR, file.path)
43
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
44
+
45
+ // Merge minecraft tag files (tick.json, load.json) instead of overwriting
46
+ if (file.path.includes('data/minecraft/tags/') && fs.existsSync(filePath)) {
47
+ const existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
48
+ const incoming = JSON.parse(file.content)
49
+ const merged = { values: [...new Set([...(existing.values ?? []), ...(incoming.values ?? [])])] }
50
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2))
51
+ } else {
52
+ fs.writeFileSync(filePath, file.content)
53
+ }
54
+ }
55
+ }
56
+
57
+ beforeAll(async () => {
58
+ mc = new MCTestClient(MC_HOST, MC_PORT)
59
+ serverOnline = await mc.isOnline()
60
+ if (!serverOnline) {
61
+ console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`)
62
+ console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`)
63
+ console.warn(` Then restart the MC server and re-run tests.`)
64
+ return
65
+ }
66
+
67
+ // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
68
+ // counter.rs
69
+ if (fs.existsSync(path.join(__dirname, '../examples/counter.rs'))) {
70
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.rs'), 'utf-8'), 'counter')
71
+ }
72
+ if (fs.existsSync(path.join(__dirname, '../examples/world_manager.rs'))) {
73
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.rs'), 'utf-8'), 'world_manager')
74
+ }
75
+ writeFixture(`
76
+ @tick
77
+ fn on_tick() {
78
+ scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
79
+ }
80
+ `, 'tick_test')
81
+ writeFixture(`
82
+ fn check_score() {
83
+ let x: int = scoreboard_get("#check_x", "test_score");
84
+ if (x > 5) {
85
+ scoreboard_set("#check_x", "result", 1);
86
+ } else {
87
+ scoreboard_set("#check_x", "result", 0);
88
+ }
89
+ }
90
+ `, 'inline_test')
91
+
92
+ // ── E2E scenario fixtures ────────────────────────────────────────────
93
+
94
+ // Scenario A: mini game loop (timer countdown + ended flag)
95
+ writeFixture(`
96
+ @tick
97
+ fn game_tick() {
98
+ let time: int = scoreboard_get("#game", "timer");
99
+ if (time > 0) {
100
+ scoreboard_set("#game", "timer", time - 1);
101
+ }
102
+ if (time == 1) {
103
+ scoreboard_set("#game", "ended", 1);
104
+ }
105
+ }
106
+ fn start_game() {
107
+ scoreboard_set("#game", "timer", 5);
108
+ scoreboard_set("#game", "ended", 0);
109
+ }
110
+ `, 'game_loop')
111
+
112
+ // Scenario B: two functions, same temp var namespace — verify no collision
113
+ writeFixture(`
114
+ fn calc_sum() {
115
+ let a: int = scoreboard_get("#math", "val_a");
116
+ let b: int = scoreboard_get("#math", "val_b");
117
+ scoreboard_set("#math", "sum", a + b);
118
+ }
119
+ fn calc_product() {
120
+ let x: int = scoreboard_get("#math", "val_x");
121
+ let y: int = scoreboard_get("#math", "val_y");
122
+ scoreboard_set("#math", "product", x * y);
123
+ }
124
+ fn run_both() {
125
+ calc_sum();
126
+ calc_product();
127
+ }
128
+ `, 'math_test')
129
+
130
+ // Scenario C: 3-deep call chain, each step modifies shared state
131
+ writeFixture(`
132
+ fn step3() {
133
+ let v: int = scoreboard_get("#chain", "val");
134
+ scoreboard_set("#chain", "val", v * 2);
135
+ }
136
+ fn step2() {
137
+ let v: int = scoreboard_get("#chain", "val");
138
+ scoreboard_set("#chain", "val", v + 5);
139
+ step3();
140
+ }
141
+ fn step1() {
142
+ scoreboard_set("#chain", "val", 10);
143
+ step2();
144
+ }
145
+ `, 'call_chain')
146
+
147
+ // Scenario D: setblock batching optimizer — 4 adjacent setblocks → fill
148
+ writeFixture(`
149
+ fn build_row() {
150
+ setblock((0, 70, 0), "minecraft:stone");
151
+ setblock((1, 70, 0), "minecraft:stone");
152
+ setblock((2, 70, 0), "minecraft:stone");
153
+ setblock((3, 70, 0), "minecraft:stone");
154
+ }
155
+ `, 'fill_test')
156
+
157
+ // ── Full reset + safe data reload ────────────────────────────────────
158
+ await mc.fullReset()
159
+
160
+ // Pre-create scoreboards
161
+ for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
162
+ 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val']) {
163
+ await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => {})
164
+ }
165
+ await mc.command('/scoreboard players set counter ticks 0')
166
+ await mc.command('/scoreboard players set #tick_counter ticks 0')
167
+ await mc.command('/scoreboard players set #check_x test_score 10')
168
+ await mc.command('/scoreboard players set #check_x result 99')
169
+
170
+ // Safe reload (Bukkit.reloadData — only datapacks, no plugin restart)
171
+ console.log(' Reloading datapacks (safe reloadData)...')
172
+ await mc.reload()
173
+ await new Promise(r => setTimeout(r, 5000)) // wall-clock wait for data reload
174
+
175
+ // Initialize __load functions
176
+ await mc.command('/function counter:__load').catch(() => {})
177
+ await mc.command('/function inline_test:__load').catch(() => {})
178
+ await mc.ticks(20)
179
+
180
+ console.log(' Setup complete.')
181
+ }, 60000)
182
+
183
+ describe('MC Integration Tests', () => {
184
+
185
+ // ─── Test 1: Server connectivity ─────────────────────────────────────
186
+ test('server is online and healthy', async () => {
187
+ if (!serverOnline) return
188
+ const status = await mc.status()
189
+ expect(status.online).toBe(true)
190
+ expect(status.tps_1m).toBeGreaterThan(10) // Allow recovery after reload
191
+ console.log(` Server: ${status.version}, TPS: ${status.tps_1m.toFixed(1)}`)
192
+ })
193
+
194
+ // ─── Test 2: Counter tick ─────────────────────────────────────────────
195
+ test('counter.rs: tick function increments scoreboard over time', async () => {
196
+ if (!serverOnline) return
197
+
198
+ await mc.ticks(40) // Wait 2s (counter was already init'd in beforeAll)
199
+ const count = await mc.scoreboard('counter', 'ticks')
200
+ expect(count).toBeGreaterThan(0)
201
+ console.log(` counter/ticks after setup+40 ticks: ${count}`)
202
+ })
203
+
204
+ // ─── Test 3: setblock ────────────────────────────────────────────────
205
+ test('world_manager.rs: setblock places correct block', async () => {
206
+ if (!serverOnline) return
207
+
208
+ // Clear just the lobby area, keep other state
209
+ await mc.fullReset({ x1: -10, y1: 60, z1: -10, x2: 15, y2: 80, z2: 15, resetScoreboards: false })
210
+ await mc.command('/function world_manager:__load')
211
+ await mc.command('/function world_manager:reset_lobby_platform')
212
+ await mc.ticks(10)
213
+
214
+ const block = await mc.block(4, 65, 4)
215
+ expect(block.type).toBe('minecraft:gold_block')
216
+ console.log(` Block at (4,65,4): ${block.type}`)
217
+ })
218
+
219
+ // ─── Test 4: fill ────────────────────────────────────────────────────
220
+ test('world_manager.rs: fill creates smooth_stone floor', async () => {
221
+ if (!serverOnline) return
222
+ // Runs after test 3, floor should still be there
223
+ const block = await mc.block(4, 64, 4)
224
+ expect(block.type).toBe('minecraft:smooth_stone')
225
+ console.log(` Floor at (4,64,4): ${block.type}`)
226
+ })
227
+
228
+ // ─── Test 5: Scoreboard arithmetic ───────────────────────────────────
229
+ test('scoreboard arithmetic works via commands', async () => {
230
+ if (!serverOnline) return
231
+
232
+ await mc.command('/scoreboard players set TestA calc 10')
233
+ await mc.command('/scoreboard players set TestB calc 25')
234
+ await mc.command('/scoreboard players operation TestA calc += TestB calc')
235
+ await mc.ticks(2)
236
+
237
+ const result = await mc.scoreboard('TestA', 'calc')
238
+ expect(result).toBe(35)
239
+ console.log(` 10 + 25 = ${result}`)
240
+ })
241
+
242
+ // ─── Test 6: Scoreboard proxy for announce ────────────────────────────
243
+ test('scoreboard proxy test (chat logging not supported for /say)', async () => {
244
+ if (!serverOnline) return
245
+
246
+ await mc.command('/scoreboard objectives add announce_test dummy')
247
+ await mc.command('/scoreboard players set announce_marker announce_test 42')
248
+ await mc.ticks(2)
249
+
250
+ const marker = await mc.scoreboard('announce_marker', 'announce_test')
251
+ expect(marker).toBe(42)
252
+ console.log(` Marker value: ${marker}`)
253
+ })
254
+
255
+ // ─── Test 7: if/else logic via inline script ──────────────────────────
256
+ test('inline rs: if/else (x=10 > 5) sets result=1', async () => {
257
+ if (!serverOnline) return
258
+
259
+ // #check_x test_score=10 was set in beforeAll, run check_score
260
+ await mc.command('/function inline_test:check_score')
261
+ await mc.ticks(5)
262
+
263
+ const result = await mc.scoreboard('#check_x', 'result')
264
+ expect(result).toBe(1)
265
+ console.log(` if (10 > 5) → result: ${result}`)
266
+ })
267
+
268
+ // ─── Test 8: Entity counting ──────────────────────────────────────────
269
+ test('entity query: armor_stands survive peaceful mode', async () => {
270
+ if (!serverOnline) return
271
+
272
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
273
+
274
+ await mc.command('/summon minecraft:armor_stand 0 65 0')
275
+ await mc.command('/summon minecraft:armor_stand 2 65 0')
276
+ await mc.command('/summon minecraft:armor_stand 4 65 0')
277
+ await mc.ticks(5)
278
+
279
+ const stands = await mc.entities('@e[type=minecraft:armor_stand]')
280
+ expect(stands.length).toBe(3)
281
+ console.log(` Spawned 3 armor_stands, found: ${stands.length}`)
282
+
283
+ await mc.command('/kill @e[type=minecraft:armor_stand]')
284
+ })
285
+
286
+ // ─── Test 9: @tick dispatcher runs every tick ─────────────────────────
287
+ test('@tick: tick_test increments #tick_counter every tick', async () => {
288
+ if (!serverOnline) return
289
+
290
+ // Reset counter
291
+ await mc.command('/scoreboard players set #tick_counter ticks 0')
292
+ await mc.ticks(40) // 2s
293
+
294
+ const ticks = await mc.scoreboard('#tick_counter', 'ticks')
295
+ expect(ticks).toBeGreaterThanOrEqual(10) // At least 10 of 40 ticks fired
296
+ console.log(` #tick_counter after 40 ticks: ${ticks}`)
297
+ })
298
+
299
+ // ─── Test 10: fullReset clears blocks ─────────────────────────────────
300
+ test('fullReset clears previously placed blocks', async () => {
301
+ if (!serverOnline) return
302
+
303
+ await mc.command('/setblock 5 65 5 minecraft:diamond_block')
304
+ await mc.ticks(2)
305
+
306
+ let block = await mc.block(5, 65, 5)
307
+ expect(block.type).toBe('minecraft:diamond_block')
308
+
309
+ await mc.fullReset({ x1: 0, y1: 60, z1: 0, x2: 10, y2: 75, z2: 10, resetScoreboards: false })
310
+ block = await mc.block(5, 65, 5)
311
+ expect(block.type).toBe('minecraft:air')
312
+ console.log(` Block after reset: ${block.type} ✓`)
313
+ })
314
+
315
+ })
316
+
317
+ // ─── E2E Scenario Tests ───────────────────────────────────────────────────────
318
+ describe('E2E Scenario Tests', () => {
319
+
320
+ // Scenario A: Mini game loop
321
+ // Verifies: @tick auto-runs, scoreboard read-modify-write, two if conditions
322
+ // in the same function, timer countdown converges to ended=1
323
+ test('A: game_loop timer countdown sets ended=1 after N ticks', async () => {
324
+ if (!serverOnline) return
325
+
326
+ // game_tick is @tick - it runs every server tick automatically.
327
+ // start_game sets timer=5, but game_tick may already decrement it by the
328
+ // time we query. Use a large timer and just verify it reaches 0 eventually.
329
+ await mc.command('/scoreboard players set #game timer 0')
330
+ await mc.command('/scoreboard players set #game ended 0')
331
+ await mc.ticks(2)
332
+
333
+ await mc.command('/function game_loop:__load')
334
+ await mc.command('/function game_loop:start_game') // timer=5, ended=0
335
+
336
+ // Wait 25 ticks — enough for 5 decrements + margin
337
+ await mc.ticks(25)
338
+
339
+ const ended = await mc.scoreboard('#game', 'ended')
340
+ expect(ended).toBe(1)
341
+ const finalTimer = await mc.scoreboard('#game', 'timer')
342
+ expect(finalTimer).toBe(0)
343
+ console.log(` timer hit 0 (final=${finalTimer}), ended=${ended} ✓`)
344
+ })
345
+
346
+ // Scenario B: No temp var collision between two functions called in sequence
347
+ // Verifies: each function's $t0/$t1 temp vars are isolated per-call, not globally shared
348
+ // If there's a bug, calc_product would see sum's leftover $t vars and produce wrong result
349
+ test('B: calc_sum + calc_product called in sequence — no temp var collision', async () => {
350
+ if (!serverOnline) return
351
+
352
+ await mc.command('/function math_test:__load')
353
+ await mc.command('/scoreboard players set #math val_a 7')
354
+ await mc.command('/scoreboard players set #math val_b 3')
355
+ await mc.command('/scoreboard players set #math val_x 4')
356
+ await mc.command('/scoreboard players set #math val_y 5')
357
+
358
+ await mc.command('/function math_test:run_both') // calc_sum() then calc_product()
359
+ await mc.ticks(5)
360
+
361
+ const sum = await mc.scoreboard('#math', 'sum')
362
+ const product = await mc.scoreboard('#math', 'product')
363
+ expect(sum).toBe(10) // 7 + 3
364
+ expect(product).toBe(20) // 4 × 5
365
+ console.log(` sum=${sum} (expect 10), product=${product} (expect 20) ✓`)
366
+ })
367
+
368
+ // Scenario C: 3-deep call chain, shared state threaded through
369
+ // Verifies: function calls preserve scoreboard state across stack frames
370
+ // step1: val=10 → step2: val=10+5=15 → step3: val=15×2=30
371
+ test('C: 3-deep call chain preserves intermediate state (10→15→30)', async () => {
372
+ if (!serverOnline) return
373
+
374
+ await mc.command('/function call_chain:__load')
375
+ await mc.command('/scoreboard players set #chain val 0')
376
+
377
+ await mc.command('/function call_chain:step1')
378
+ await mc.ticks(5)
379
+
380
+ const val = await mc.scoreboard('#chain', 'val')
381
+ expect(val).toBe(30) // (10 + 5) * 2 = 30
382
+ console.log(` call chain result: ${val} (expect 30) ✓`)
383
+ })
384
+
385
+ // Scenario D: Setblock batching optimizer — 4 adjacent setblocks compiled to fill
386
+ // Verifies: optimizer's fill-batching pass produces correct MC behavior
387
+ // (not just that the output says "fill", but that ALL 4 blocks are actually stone)
388
+ test('D: fill optimizer — 4 adjacent setblocks all placed correctly', async () => {
389
+ if (!serverOnline) return
390
+
391
+ await mc.fullReset({ x1: -5, y1: 65, z1: -5, x2: 10, y2: 75, z2: 10, resetScoreboards: false })
392
+ await mc.command('/function fill_test:__load')
393
+ await mc.command('/function fill_test:build_row')
394
+ await mc.ticks(5)
395
+
396
+ // All 4 blocks should be stone (optimizer batched into fill 0 70 0 3 70 0 stone)
397
+ for (let x = 0; x <= 3; x++) {
398
+ const block = await mc.block(x, 70, 0)
399
+ expect(block.type).toBe('minecraft:stone')
400
+ }
401
+ // Neighbors should still be air (fill didn't overshoot)
402
+ const before = await mc.block(-1, 70, 0)
403
+ const after = await mc.block(4, 70, 0)
404
+ expect(before.type).toBe('minecraft:air')
405
+ expect(after.type).toBe('minecraft:air')
406
+ console.log(` fill_test: blocks [0-3,70,0]=stone, [-1]/[4]=air ✓`)
407
+ })
408
+
409
+ })
@@ -0,0 +1,96 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+
4
+ import { compile } from '../compile'
5
+ import { MCCommandValidator } from '../mc-validator'
6
+
7
+ const FIXTURE_PATH = path.join(__dirname, 'fixtures', 'mc-commands-1.21.4.json')
8
+ const EXAMPLES = ['counter', 'arena', 'shop', 'quiz', 'turret']
9
+
10
+ function getCommands(source: string, namespace = 'test'): string[] {
11
+ const result = compile(source, { namespace })
12
+ expect(result.success).toBe(true)
13
+ expect(result.files).toBeDefined()
14
+
15
+ return (result.files ?? [])
16
+ .filter(file => file.path.endsWith('.mcfunction'))
17
+ .flatMap(file => file.content.split('\n'))
18
+ .filter(line => line.trim().length > 0)
19
+ }
20
+
21
+ function validateSource(
22
+ validator: MCCommandValidator,
23
+ source: string,
24
+ namespace: string
25
+ ): Array<{ cmd: string, error?: string }> {
26
+ return getCommands(source, namespace)
27
+ .map(cmd => ({ cmd, result: validator.validate(cmd) }))
28
+ .filter(entry => !entry.result.valid)
29
+ .map(entry => ({ cmd: entry.cmd, error: entry.result.error }))
30
+ }
31
+
32
+ describe('MC Command Syntax Validation', () => {
33
+ const validator = new MCCommandValidator(FIXTURE_PATH)
34
+
35
+ test('counter example generates valid MC commands', () => {
36
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.rs'), 'utf-8')
37
+ const errors = validateSource(validator, src, 'counter')
38
+ expect(errors).toHaveLength(0)
39
+ })
40
+
41
+ EXAMPLES.forEach(name => {
42
+ test(`${name}.rs generates valid MC commands`, () => {
43
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.rs`), 'utf-8')
44
+ const errors = validateSource(validator, src, name)
45
+
46
+ if (errors.length > 0) {
47
+ console.log('Invalid commands:', errors)
48
+ }
49
+
50
+ expect(errors).toHaveLength(0)
51
+ })
52
+ })
53
+
54
+ test('string interpolation generates valid tellraw', () => {
55
+ const errors = validateSource(validator, `
56
+ fn chat() {
57
+ let score: int = 7;
58
+ say("You have \${score} points");
59
+ }
60
+ `, 'interpolation')
61
+
62
+ expect(errors).toHaveLength(0)
63
+ })
64
+
65
+ test('array operations generate valid data commands', () => {
66
+ const errors = validateSource(validator, `
67
+ fn arrays() {
68
+ let arr: int[] = [];
69
+ arr.push(4);
70
+ arr.push(9);
71
+ let popped: int = arr.pop();
72
+ let len: int = arr.len;
73
+
74
+ scoreboard_set("arrays", "len", len);
75
+ scoreboard_set("arrays", "last", popped);
76
+ }
77
+ `, 'arrays')
78
+
79
+ expect(errors).toHaveLength(0)
80
+ })
81
+
82
+ test('match generates valid execute commands', () => {
83
+ const errors = validateSource(validator, `
84
+ fn choose() {
85
+ let choice: int = 2;
86
+ match (choice) {
87
+ 1 => { say("one"); }
88
+ 2 => { say("two"); }
89
+ _ => { say("other"); }
90
+ }
91
+ }
92
+ `, 'matching')
93
+
94
+ expect(errors).toHaveLength(0)
95
+ })
96
+ })
@@ -0,0 +1,58 @@
1
+ import * as fs from 'fs'
2
+
3
+ import { compileToStructure } from '../codegen/structure'
4
+ import { nbt, readNbt, TagType, writeNbt, type CompoundTag } from '../nbt'
5
+
6
+ describe('NBT codec', () => {
7
+ test('round-trips a compound tag', () => {
8
+ const tag = nbt.compound({ x: nbt.int(42), name: nbt.string('test') })
9
+ const buf = writeNbt(tag, 'root')
10
+ const parsed = readNbt(buf)
11
+
12
+ expect(parsed.name).toBe('root')
13
+ expect(parsed.tag).toEqual(tag)
14
+ })
15
+
16
+ test('round-trips nested lists and arrays', () => {
17
+ const tag = nbt.compound({
18
+ nested: nbt.list(TagType.Compound, [
19
+ nbt.compound({ values: nbt.intArray([1, 2, 3]) }),
20
+ nbt.compound({ bytes: nbt.byteArray([-1, 0, 1]) }),
21
+ ]),
22
+ longs: { type: TagType.LongArray, value: BigInt64Array.from([1n, 2n, 3n]) },
23
+ })
24
+
25
+ const buf = writeNbt(tag, 'root')
26
+ const parsed = readNbt(buf)
27
+
28
+ expect(parsed.tag).toEqual(tag)
29
+ })
30
+
31
+ test('handles longs correctly', () => {
32
+ const tag = nbt.compound({ ts: nbt.long(9007199254740993n) })
33
+ const buf = writeNbt(tag, '')
34
+ const parsed = readNbt(buf)
35
+ const root = parsed.tag as CompoundTag
36
+
37
+ expect(root.entries.get('ts')).toEqual(nbt.long(9007199254740993n))
38
+ expect((root.entries.get('ts') as { value: bigint }).value).toBe(9007199254740993n)
39
+ })
40
+ })
41
+
42
+ describe('Structure generator', () => {
43
+ test('compiles counter.rs to a non-empty structure', () => {
44
+ const filePath = 'src/examples/counter.rs'
45
+ const src = fs.readFileSync(filePath, 'utf-8')
46
+ const { buffer, blockCount } = compileToStructure(src, 'counter', filePath)
47
+
48
+ expect(buffer.length).toBeGreaterThan(100)
49
+ expect(blockCount).toBeGreaterThan(0)
50
+
51
+ const parsed = readNbt(buffer)
52
+ const root = parsed.tag as CompoundTag
53
+ const blocks = root.entries.get('blocks')
54
+
55
+ expect(parsed.name).toBe('')
56
+ expect(blocks?.type).toBe(TagType.List)
57
+ })
58
+ })