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,1120 @@
1
+ import * as vscode from 'vscode'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Builtin documentation database
5
+ // ---------------------------------------------------------------------------
6
+
7
+ interface BuiltinDoc {
8
+ signature: string
9
+ description: string
10
+ params?: { name: string; type: string; optional?: boolean; desc: string }[]
11
+ returns?: string
12
+ example?: string
13
+ mc?: string // compiled MC command
14
+ }
15
+
16
+ const BUILTINS: Record<string, BuiltinDoc> = {
17
+ // --- Chat & Display ---
18
+ say: {
19
+ signature: 'say(msg: string)',
20
+ description: 'Broadcast a message to all players as the server.',
21
+ params: [{ name: 'msg', type: 'string', desc: 'Message to broadcast' }],
22
+ example: 'say("Hello world!");',
23
+ mc: 'say <msg>'
24
+ },
25
+ tell: {
26
+ signature: 'tell(target: selector, msg: string)',
27
+ description: 'Send a private message to a player or selector.',
28
+ params: [
29
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
30
+ { name: 'msg', type: 'string', desc: 'Message to send' }
31
+ ],
32
+ example: 'tell(@s, "You scored a point!");',
33
+ mc: 'tellraw <target> {"text":"<msg>"}'
34
+ },
35
+ announce: {
36
+ signature: 'announce(msg: string)',
37
+ description: 'Send a message to all players in chat.',
38
+ params: [{ name: 'msg', type: 'string', desc: 'Message text' }],
39
+ example: 'announce("Game over!");',
40
+ mc: 'tellraw @a {"text":"<msg>"}'
41
+ },
42
+ title: {
43
+ signature: 'title(target: selector, msg: string)',
44
+ description: 'Show a large title on screen for target players.',
45
+ params: [
46
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
47
+ { name: 'msg', type: 'string', desc: 'Title text' }
48
+ ],
49
+ example: 'title(@a, "Round 1");',
50
+ mc: 'title <target> title {"text":"<msg>"}'
51
+ },
52
+ subtitle: {
53
+ signature: 'subtitle(target: selector, msg: string)',
54
+ description: 'Show subtitle text below the title.',
55
+ params: [
56
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
57
+ { name: 'msg', type: 'string', desc: 'Subtitle text' }
58
+ ],
59
+ example: 'subtitle(@a, "Fight!");',
60
+ mc: 'title <target> subtitle {"text":"<msg>"}'
61
+ },
62
+ actionbar: {
63
+ signature: 'actionbar(target: selector, msg: string)',
64
+ description: 'Show text in the action bar (above hotbar).',
65
+ params: [
66
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
67
+ { name: 'msg', type: 'string', desc: 'Action bar text' }
68
+ ],
69
+ example: 'actionbar(@a, "⏱ ${time}s remaining");',
70
+ mc: 'title <target> actionbar {"text":"<msg>"}'
71
+ },
72
+ title_times: {
73
+ signature: 'title_times(target: selector, fadeIn: int, stay: int, fadeOut: int)',
74
+ description: 'Set title display timing (in ticks).',
75
+ params: [
76
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
77
+ { name: 'fadeIn', type: 'int', desc: 'Fade-in ticks' },
78
+ { name: 'stay', type: 'int', desc: 'Stay ticks' },
79
+ { name: 'fadeOut', type: 'int', desc: 'Fade-out ticks' }
80
+ ],
81
+ example: 'title_times(@a, 10, 40, 10);',
82
+ mc: 'title <target> times <fadeIn> <stay> <fadeOut>'
83
+ },
84
+
85
+ // --- Player ---
86
+ give: {
87
+ signature: 'give(target: selector, item: string, count?: int)',
88
+ description: 'Give item(s) to a player.',
89
+ params: [
90
+ { name: 'target', type: 'selector', desc: 'Target player(s)' },
91
+ { name: 'item', type: 'string', desc: 'Item ID (e.g. "minecraft:diamond")' },
92
+ { name: 'count', type: 'int', optional: true, desc: 'Amount (default: 1)' }
93
+ ],
94
+ example: 'give(@s, "minecraft:diamond", 5);',
95
+ mc: 'give <target> <item> [count]'
96
+ },
97
+ kill: {
98
+ signature: 'kill(target?: selector)',
99
+ description: 'Kill entity/entities. Defaults to @s.',
100
+ params: [{ name: 'target', type: 'selector', optional: true, desc: 'Target (default: @s)' }],
101
+ example: 'kill(@e[type=minecraft:zombie]);',
102
+ mc: 'kill [target]'
103
+ },
104
+ effect: {
105
+ signature: 'effect(target: selector, effect: string, duration?: int, amplifier?: int)',
106
+ description: 'Apply a status effect.',
107
+ params: [
108
+ { name: 'target', type: 'selector', desc: 'Target entity/player' },
109
+ { name: 'effect', type: 'string', desc: 'Effect ID (e.g. "minecraft:speed")' },
110
+ { name: 'duration', type: 'int', optional: true, desc: 'Seconds (default: 30)' },
111
+ { name: 'amplifier', type: 'int', optional: true, desc: 'Level 0-255 (default: 0)' }
112
+ ],
113
+ example: 'effect(@s, "minecraft:speed", 60, 1);',
114
+ mc: 'effect give <target> <effect> [duration] [amplifier]'
115
+ },
116
+ clear: {
117
+ signature: 'clear(target: selector, item?: string)',
118
+ description: 'Remove items from inventory.',
119
+ params: [
120
+ { name: 'target', type: 'selector', desc: 'Target player' },
121
+ { name: 'item', type: 'string', optional: true, desc: 'Specific item to remove (default: all)' }
122
+ ],
123
+ example: 'clear(@s, "minecraft:dirt");',
124
+ mc: 'clear <target> [item]'
125
+ },
126
+ kick: {
127
+ signature: 'kick(player: selector, reason?: string)',
128
+ description: 'Kick a player from the server.',
129
+ params: [
130
+ { name: 'player', type: 'selector', desc: 'Target player' },
131
+ { name: 'reason', type: 'string', optional: true, desc: 'Kick message' }
132
+ ],
133
+ example: 'kick(@s, "You lost!");',
134
+ mc: 'kick <player> [reason]'
135
+ },
136
+ xp_add: {
137
+ signature: 'xp_add(target: selector, amount: int, type?: string)',
138
+ description: 'Add experience to a player.',
139
+ params: [
140
+ { name: 'target', type: 'selector', desc: 'Target player' },
141
+ { name: 'amount', type: 'int', desc: 'Amount to add' },
142
+ { name: 'type', type: 'string', optional: true, desc: '"points" or "levels" (default: "points")' }
143
+ ],
144
+ example: 'xp_add(@s, 100);',
145
+ mc: 'xp add <target> <amount> [type]'
146
+ },
147
+ xp_set: {
148
+ signature: 'xp_set(target: selector, amount: int, type?: string)',
149
+ description: 'Set a player\'s experience.',
150
+ params: [
151
+ { name: 'target', type: 'selector', desc: 'Target player' },
152
+ { name: 'amount', type: 'int', desc: 'New value' },
153
+ { name: 'type', type: 'string', optional: true, desc: '"points" or "levels"' }
154
+ ],
155
+ example: 'xp_set(@s, 0, "levels");',
156
+ mc: 'xp set <target> <amount> [type]'
157
+ },
158
+
159
+ // --- Teleport ---
160
+ tp: {
161
+ signature: 'tp(target: selector, destination: selector | BlockPos)',
162
+ description: 'Teleport entity to a player or coordinates.',
163
+ params: [
164
+ { name: 'target', type: 'selector', desc: 'Entity to teleport' },
165
+ { name: 'destination', type: 'selector | BlockPos', desc: 'Target player or position' }
166
+ ],
167
+ example: 'tp(@s, (0, 64, 0));\ntp(@a, @s);',
168
+ mc: 'tp <target> <dest>'
169
+ },
170
+
171
+ // --- World ---
172
+ setblock: {
173
+ signature: 'setblock(pos: BlockPos, block: string)',
174
+ description: 'Place a block at coordinates.',
175
+ params: [
176
+ { name: 'pos', type: 'BlockPos', desc: 'Target position e.g. (0, 64, 0) or (~1, ~0, ~0)' },
177
+ { name: 'block', type: 'string', desc: 'Block ID (e.g. "minecraft:stone")' }
178
+ ],
179
+ example: 'setblock((0, 64, 0), "minecraft:stone");',
180
+ mc: 'setblock <x> <y> <z> <block>'
181
+ },
182
+ fill: {
183
+ signature: 'fill(from: BlockPos, to: BlockPos, block: string)',
184
+ description: 'Fill a region with blocks.',
185
+ params: [
186
+ { name: 'from', type: 'BlockPos', desc: 'Start corner' },
187
+ { name: 'to', type: 'BlockPos', desc: 'End corner' },
188
+ { name: 'block', type: 'string', desc: 'Block to fill with' }
189
+ ],
190
+ example: 'fill((0, 64, 0), (10, 64, 10), "minecraft:grass_block");',
191
+ mc: 'fill <x1> <y1> <z1> <x2> <y2> <z2> <block>'
192
+ },
193
+ clone: {
194
+ signature: 'clone(from: BlockPos, to: BlockPos, dest: BlockPos)',
195
+ description: 'Clone a region of blocks to a new location.',
196
+ params: [
197
+ { name: 'from', type: 'BlockPos', desc: 'Source start corner' },
198
+ { name: 'to', type: 'BlockPos', desc: 'Source end corner' },
199
+ { name: 'dest', type: 'BlockPos', desc: 'Destination corner' }
200
+ ],
201
+ example: 'clone((0,64,0), (10,64,10), (20,64,0));',
202
+ mc: 'clone <x1> <y1> <z1> <x2> <y2> <z2> <dx> <dy> <dz>'
203
+ },
204
+ summon: {
205
+ signature: 'summon(type: string, pos: BlockPos)',
206
+ description: 'Spawn an entity at a location.',
207
+ params: [
208
+ { name: 'type', type: 'string', desc: 'Entity type ID (e.g. "minecraft:zombie")' },
209
+ { name: 'pos', type: 'BlockPos', desc: 'Spawn position' }
210
+ ],
211
+ example: 'summon("minecraft:zombie", (0, 64, 0));',
212
+ mc: 'summon <type> <x> <y> <z>'
213
+ },
214
+ weather: {
215
+ signature: 'weather(type: string)',
216
+ description: 'Set the weather.',
217
+ params: [{ name: 'type', type: 'string', desc: '"clear", "rain", or "thunder"' }],
218
+ example: 'weather("clear");',
219
+ mc: 'weather <type>'
220
+ },
221
+ time_set: {
222
+ signature: 'time_set(value: int | string)',
223
+ description: 'Set the world time.',
224
+ params: [{ name: 'value', type: 'int | string', desc: 'Time in ticks, or "day"/"night"/"noon"/"midnight"' }],
225
+ example: 'time_set(0); // dawn\ntime_set("noon");',
226
+ mc: 'time set <value>'
227
+ },
228
+ time_add: {
229
+ signature: 'time_add(ticks: int)',
230
+ description: 'Advance world time by ticks.',
231
+ params: [{ name: 'ticks', type: 'int', desc: 'Ticks to add' }],
232
+ example: 'time_add(6000);',
233
+ mc: 'time add <ticks>'
234
+ },
235
+ gamerule: {
236
+ signature: 'gamerule(rule: string, value: bool | int)',
237
+ description: 'Set a gamerule value.',
238
+ params: [
239
+ { name: 'rule', type: 'string', desc: 'Gamerule name (e.g. "keepInventory")' },
240
+ { name: 'value', type: 'bool | int', desc: 'New value' }
241
+ ],
242
+ example: 'gamerule("keepInventory", true);\ngamerule("randomTickSpeed", 3);',
243
+ mc: 'gamerule <rule> <value>'
244
+ },
245
+ difficulty: {
246
+ signature: 'difficulty(level: string)',
247
+ description: 'Set the game difficulty.',
248
+ params: [{ name: 'level', type: 'string', desc: '"peaceful", "easy", "normal", or "hard"' }],
249
+ example: 'difficulty("hard");',
250
+ mc: 'difficulty <level>'
251
+ },
252
+ particle: {
253
+ signature: 'particle(name: string, pos: BlockPos)',
254
+ description: 'Spawn a particle effect.',
255
+ params: [
256
+ { name: 'name', type: 'string', desc: 'Particle type (e.g. "minecraft:flame")' },
257
+ { name: 'pos', type: 'BlockPos', desc: 'Position' }
258
+ ],
259
+ example: 'particle("minecraft:flame", (~0, ~1, ~0));',
260
+ mc: 'particle <name> <x> <y> <z>'
261
+ },
262
+ playsound: {
263
+ signature: 'playsound(sound: string, source: string, target: selector, pos?: BlockPos, volume?: float, pitch?: float)',
264
+ description: 'Play a sound for a player.',
265
+ params: [
266
+ { name: 'sound', type: 'string', desc: 'Sound event ID' },
267
+ { name: 'source', type: 'string', desc: 'Category: "master", "music", "record", "weather", "block", "hostile", "neutral", "player", "ambient", "voice"' },
268
+ { name: 'target', type: 'selector', desc: 'Target player' },
269
+ { name: 'pos', type: 'BlockPos', optional: true, desc: 'Origin position' },
270
+ { name: 'volume', type: 'float', optional: true, desc: 'Volume (default: 1.0)' },
271
+ { name: 'pitch', type: 'float', optional: true, desc: 'Pitch (default: 1.0)' }
272
+ ],
273
+ example: 'playsound("entity.experience_orb.pickup", "player", @s);',
274
+ mc: 'playsound <sound> <source> <target>'
275
+ },
276
+
277
+ // --- Tags ---
278
+ tag_add: {
279
+ signature: 'tag_add(target: selector, tag: string)',
280
+ description: 'Add an entity tag.',
281
+ params: [
282
+ { name: 'target', type: 'selector', desc: 'Target entity' },
283
+ { name: 'tag', type: 'string', desc: 'Tag name' }
284
+ ],
285
+ example: 'tag_add(@s, "hasKey");',
286
+ mc: 'tag <target> add <tag>'
287
+ },
288
+ tag_remove: {
289
+ signature: 'tag_remove(target: selector, tag: string)',
290
+ description: 'Remove an entity tag.',
291
+ params: [
292
+ { name: 'target', type: 'selector', desc: 'Target entity' },
293
+ { name: 'tag', type: 'string', desc: 'Tag name' }
294
+ ],
295
+ example: 'tag_remove(@s, "hasKey");',
296
+ mc: 'tag <target> remove <tag>'
297
+ },
298
+
299
+ // --- Scoreboard ---
300
+ scoreboard_get: {
301
+ signature: 'scoreboard_get(target: selector | string, objective: string) -> int',
302
+ description: 'Read a scoreboard value.',
303
+ params: [
304
+ { name: 'target', type: 'selector | string', desc: 'Player/entity or fake player name (e.g. "#counter")' },
305
+ { name: 'objective', type: 'string', desc: 'Scoreboard objective name' }
306
+ ],
307
+ returns: 'int',
308
+ example: 'let hp: int = scoreboard_get(@s, "health");',
309
+ mc: 'scoreboard players get <target> <objective>'
310
+ },
311
+ score: {
312
+ signature: 'score(target: selector | string, objective: string) -> int',
313
+ description: 'Alias for scoreboard_get. Read a scoreboard value.',
314
+ params: [
315
+ { name: 'target', type: 'selector | string', desc: 'Player/entity or fake player name' },
316
+ { name: 'objective', type: 'string', desc: 'Scoreboard objective name' }
317
+ ],
318
+ returns: 'int',
319
+ example: 'let kills: int = score(@s, "kills");',
320
+ mc: 'scoreboard players get <target> <objective>'
321
+ },
322
+ scoreboard_set: {
323
+ signature: 'scoreboard_set(target: selector | string, objective: string, value: int)',
324
+ description: 'Set a scoreboard value.',
325
+ params: [
326
+ { name: 'target', type: 'selector | string', desc: 'Player/entity or fake player' },
327
+ { name: 'objective', type: 'string', desc: 'Objective name' },
328
+ { name: 'value', type: 'int', desc: 'New value' }
329
+ ],
330
+ example: 'scoreboard_set("#game", "timer", 300);',
331
+ mc: 'scoreboard players set <target> <objective> <value>'
332
+ },
333
+ scoreboard_add: {
334
+ signature: 'scoreboard_add(target: selector | string, objective: string, amount: int)',
335
+ description: 'Add to a scoreboard value.',
336
+ params: [
337
+ { name: 'target', type: 'selector | string', desc: 'Player/entity or fake player' },
338
+ { name: 'objective', type: 'string', desc: 'Objective name' },
339
+ { name: 'amount', type: 'int', desc: 'Amount to add (can be negative)' }
340
+ ],
341
+ example: 'scoreboard_add(@s, "kills", 1);',
342
+ mc: 'scoreboard players add <target> <objective> <amount>'
343
+ },
344
+ scoreboard_display: {
345
+ signature: 'scoreboard_display(slot: string, objective: string)',
346
+ description: 'Display a scoreboard objective in a slot.',
347
+ params: [
348
+ { name: 'slot', type: 'string', desc: '"list", "sidebar", or "belowName"' },
349
+ { name: 'objective', type: 'string', desc: 'Objective name' }
350
+ ],
351
+ example: 'scoreboard_display("sidebar", "kills");',
352
+ mc: 'scoreboard objectives setdisplay <slot> <objective>'
353
+ },
354
+ scoreboard_add_objective: {
355
+ signature: 'scoreboard_add_objective(name: string, criteria: string)',
356
+ description: 'Create a new scoreboard objective.',
357
+ params: [
358
+ { name: 'name', type: 'string', desc: 'Objective name' },
359
+ { name: 'criteria', type: 'string', desc: 'Criteria (e.g. "dummy", "playerKillCount")' }
360
+ ],
361
+ example: 'scoreboard_add_objective("kills", "playerKillCount");',
362
+ mc: 'scoreboard objectives add <name> <criteria>'
363
+ },
364
+ scoreboard_remove_objective: {
365
+ signature: 'scoreboard_remove_objective(name: string)',
366
+ description: 'Remove a scoreboard objective.',
367
+ params: [{ name: 'name', type: 'string', desc: 'Objective name' }],
368
+ example: 'scoreboard_remove_objective("kills");',
369
+ mc: 'scoreboard objectives remove <name>'
370
+ },
371
+ scoreboard_hide: {
372
+ signature: 'scoreboard_hide(slot: string)',
373
+ description: 'Clear the display in a scoreboard slot.',
374
+ params: [{ name: 'slot', type: 'string', desc: '"list", "sidebar", or "belowName"' }],
375
+ example: 'scoreboard_hide("sidebar");',
376
+ mc: 'scoreboard objectives setdisplay <slot>'
377
+ },
378
+
379
+ // --- Random ---
380
+ random: {
381
+ signature: 'random(min: int, max: int) -> int',
382
+ description: 'Generate a random integer in range [min, max] using scoreboard arithmetic.',
383
+ params: [
384
+ { name: 'min', type: 'int', desc: 'Minimum value (inclusive)' },
385
+ { name: 'max', type: 'int', desc: 'Maximum value (inclusive)' }
386
+ ],
387
+ returns: 'int',
388
+ example: 'let roll: int = random(1, 6);',
389
+ },
390
+ random_native: {
391
+ signature: 'random_native(min: int, max: int) -> int',
392
+ description: 'Generate a random integer using /random command (MC 1.20.3+). Faster than random().',
393
+ params: [
394
+ { name: 'min', type: 'int', desc: 'Minimum value (inclusive)' },
395
+ { name: 'max', type: 'int', desc: 'Maximum value (inclusive)' }
396
+ ],
397
+ returns: 'int',
398
+ example: 'let n: int = random_native(1, 100);',
399
+ mc: 'random value <min> <max>'
400
+ },
401
+
402
+ // --- Strings ---
403
+ str_len: {
404
+ signature: 'str_len(s: string) -> int',
405
+ description: 'Get the length of a string (stored in NBT storage).',
406
+ params: [{ name: 's', type: 'string', desc: 'Input string' }],
407
+ returns: 'int',
408
+ example: 'let n: int = str_len("hello"); // 5',
409
+ },
410
+
411
+ // --- Arrays ---
412
+ push: {
413
+ signature: 'push(arr: T[], value: T)',
414
+ description: 'Append a value to the end of an array.',
415
+ params: [
416
+ { name: 'arr', type: 'T[]', desc: 'Target array' },
417
+ { name: 'value', type: 'T', desc: 'Value to append' }
418
+ ],
419
+ example: 'let scores: int[] = [];\npush(scores, 42);',
420
+ mc: 'data modify storage rs:heap <arr> append value <value>'
421
+ },
422
+ pop: {
423
+ signature: 'pop(arr: T[]) -> T',
424
+ description: 'Remove and return the last element of an array.',
425
+ params: [{ name: 'arr', type: 'T[]', desc: 'Target array' }],
426
+ returns: 'T',
427
+ example: 'let last: int = pop(scores);',
428
+ mc: 'data remove storage rs:heap <arr>[-1]'
429
+ },
430
+ len: {
431
+ signature: 'arr.len',
432
+ description: 'Get the number of elements in an array (property access, not a function call).',
433
+ example: 'let n: int = scores.len;',
434
+ },
435
+
436
+ // --- Data ---
437
+ data_get: {
438
+ signature: 'data_get(target: string, path: string) -> int',
439
+ description: 'Read NBT data from entity/block/storage.',
440
+ params: [
441
+ { name: 'target', type: 'string', desc: 'Target selector or storage path' },
442
+ { name: 'path', type: 'string', desc: 'NBT path (e.g. "Health")' }
443
+ ],
444
+ returns: 'int',
445
+ example: 'let hp: int = data_get("@s", "Health");',
446
+ mc: 'execute store result score $rs_tmp rs_tmp run data get entity <target> <path>'
447
+ },
448
+
449
+ // --- Bossbar ---
450
+ bossbar_add: {
451
+ signature: 'bossbar_add(id: string, name: string)',
452
+ description: 'Create a new boss bar.',
453
+ params: [
454
+ { name: 'id', type: 'string', desc: 'Boss bar ID (e.g. "minecraft:health")' },
455
+ { name: 'name', type: 'string', desc: 'Display name' }
456
+ ],
457
+ example: 'bossbar_add("mymod:timer", "Time Left");',
458
+ mc: 'bossbar add <id> {"text":"<name>"}'
459
+ },
460
+ bossbar_set_value: {
461
+ signature: 'bossbar_set_value(id: string, value: int)',
462
+ description: 'Set boss bar current value.',
463
+ params: [
464
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
465
+ { name: 'value', type: 'int', desc: 'Current value' }
466
+ ],
467
+ example: 'bossbar_set_value("mymod:timer", 60);',
468
+ mc: 'bossbar set <id> value <value>'
469
+ },
470
+ bossbar_set_max: {
471
+ signature: 'bossbar_set_max(id: string, max: int)',
472
+ description: 'Set boss bar maximum value.',
473
+ params: [
474
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
475
+ { name: 'max', type: 'int', desc: 'Maximum value' }
476
+ ],
477
+ example: 'bossbar_set_max("mymod:timer", 300);',
478
+ mc: 'bossbar set <id> max <max>'
479
+ },
480
+ bossbar_remove: {
481
+ signature: 'bossbar_remove(id: string)',
482
+ description: 'Remove a boss bar.',
483
+ params: [{ name: 'id', type: 'string', desc: 'Boss bar ID' }],
484
+ example: 'bossbar_remove("mymod:timer");',
485
+ mc: 'bossbar remove <id>'
486
+ },
487
+ bossbar_set_players: {
488
+ signature: 'bossbar_set_players(id: string, target: selector)',
489
+ description: 'Set which players see the boss bar.',
490
+ params: [
491
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
492
+ { name: 'target', type: 'selector', desc: 'Target players' }
493
+ ],
494
+ example: 'bossbar_set_players("mymod:timer", @a);',
495
+ mc: 'bossbar set <id> players <target>'
496
+ },
497
+ bossbar_set_color: {
498
+ signature: 'bossbar_set_color(id: string, color: string)',
499
+ description: 'Set boss bar color.',
500
+ params: [
501
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
502
+ { name: 'color', type: 'string', desc: '"blue", "green", "pink", "purple", "red", "white", "yellow"' }
503
+ ],
504
+ example: 'bossbar_set_color("mymod:timer", "red");',
505
+ mc: 'bossbar set <id> color <color>'
506
+ },
507
+ bossbar_set_style: {
508
+ signature: 'bossbar_set_style(id: string, style: string)',
509
+ description: 'Set boss bar segmentation style.',
510
+ params: [
511
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
512
+ { name: 'style', type: 'string', desc: '"notched_6", "notched_10", "notched_12", "notched_20", "progress"' }
513
+ ],
514
+ example: 'bossbar_set_style("mymod:timer", "notched_10");',
515
+ },
516
+ bossbar_set_visible: {
517
+ signature: 'bossbar_set_visible(id: string, visible: bool)',
518
+ description: 'Show or hide a boss bar.',
519
+ params: [
520
+ { name: 'id', type: 'string', desc: 'Boss bar ID' },
521
+ { name: 'visible', type: 'bool', desc: 'Visibility state' }
522
+ ],
523
+ example: 'bossbar_set_visible("mymod:timer", true);',
524
+ },
525
+ bossbar_get_value: {
526
+ signature: 'bossbar_get_value(id: string) -> int',
527
+ description: 'Get the current value of a boss bar.',
528
+ params: [{ name: 'id', type: 'string', desc: 'Boss bar ID' }],
529
+ returns: 'int',
530
+ example: 'let v: int = bossbar_get_value("mymod:timer");',
531
+ mc: 'execute store result score $rs_tmp rs_tmp run bossbar get <id> value'
532
+ },
533
+
534
+ // --- Teams ---
535
+ team_add: {
536
+ signature: 'team_add(name: string)',
537
+ description: 'Create a new team.',
538
+ params: [{ name: 'name', type: 'string', desc: 'Team name' }],
539
+ example: 'team_add("red");',
540
+ mc: 'team add <name>'
541
+ },
542
+ team_remove: {
543
+ signature: 'team_remove(name: string)',
544
+ description: 'Remove a team.',
545
+ params: [{ name: 'name', type: 'string', desc: 'Team name' }],
546
+ example: 'team_remove("red");',
547
+ mc: 'team remove <name>'
548
+ },
549
+ team_join: {
550
+ signature: 'team_join(name: string, target: selector)',
551
+ description: 'Add entities to a team.',
552
+ params: [
553
+ { name: 'name', type: 'string', desc: 'Team name' },
554
+ { name: 'target', type: 'selector', desc: 'Entities to add' }
555
+ ],
556
+ example: 'team_join("red", @s);',
557
+ mc: 'team join <name> <target>'
558
+ },
559
+ team_leave: {
560
+ signature: 'team_leave(target: selector)',
561
+ description: 'Remove entities from their team.',
562
+ params: [{ name: 'target', type: 'selector', desc: 'Entities to remove' }],
563
+ example: 'team_leave(@s);',
564
+ mc: 'team leave <target>'
565
+ },
566
+ team_option: {
567
+ signature: 'team_option(name: string, option: string, value: string)',
568
+ description: 'Set a team option.',
569
+ params: [
570
+ { name: 'name', type: 'string', desc: 'Team name' },
571
+ { name: 'option', type: 'string', desc: 'Option name (e.g. "color", "friendlyFire")' },
572
+ { name: 'value', type: 'string', desc: 'Option value' }
573
+ ],
574
+ example: 'team_option("red", "color", "red");',
575
+ mc: 'team modify <name> <option> <value>'
576
+ },
577
+
578
+ // --- Decorators ---
579
+ tick: {
580
+ signature: '@tick | @tick(rate: int)',
581
+ description: 'Run this function every tick (rate=1) or every N ticks.',
582
+ params: [{ name: 'rate', type: 'int', optional: true, desc: 'Tick interval (default: 1). @tick(rate=20) = every second.' }],
583
+ example: '@tick(rate=20)\nfn every_second() { ... }',
584
+ },
585
+ on_advancement: {
586
+ signature: '@on_advancement(id: string)',
587
+ description: 'Trigger when a player earns an advancement.',
588
+ params: [{ name: 'id', type: 'string', desc: 'Advancement ID (e.g. "story/mine_diamond")' }],
589
+ example: '@on_advancement("story/mine_diamond")\nfn got_diamond() { give(@s, "minecraft:diamond", 5); }',
590
+ },
591
+ on_death: {
592
+ signature: '@on_death',
593
+ description: 'Trigger when the executing entity dies.',
594
+ example: '@on_death\nfn died() { scoreboard_add(@s, "deaths", 1); }',
595
+ },
596
+ on_craft: {
597
+ signature: '@on_craft(item: string)',
598
+ description: 'Trigger when a player crafts an item.',
599
+ params: [{ name: 'item', type: 'string', desc: 'Crafted item ID' }],
600
+ example: '@on_craft("minecraft:diamond_sword")\nfn crafted_sword() { tell(@s, "Nice sword!"); }',
601
+ },
602
+ }
603
+
604
+ // ---------------------------------------------------------------------------
605
+ // Hover Provider
606
+ // ---------------------------------------------------------------------------
607
+
608
+ function formatDoc(doc: BuiltinDoc): vscode.MarkdownString {
609
+ const md = new vscode.MarkdownString('', true)
610
+ md.isTrusted = true
611
+ md.supportHtml = false
612
+
613
+ // Signature (code block)
614
+ md.appendCodeblock(doc.signature, 'redscript')
615
+
616
+ // Description
617
+ md.appendText('\n')
618
+ md.appendMarkdown(doc.description)
619
+ md.appendText('\n')
620
+
621
+ // Parameters
622
+ if (doc.params?.length) {
623
+ md.appendText('\n')
624
+ md.appendMarkdown('**Parameters:**\n')
625
+ for (const p of doc.params) {
626
+ const opt = p.optional ? '?' : ''
627
+ md.appendMarkdown(`- \`${p.name}${opt}: ${p.type}\` — ${p.desc}\n`)
628
+ }
629
+ }
630
+
631
+ // Return type
632
+ if (doc.returns) {
633
+ md.appendMarkdown(`\n**Returns:** \`${doc.returns}\`\n`)
634
+ }
635
+
636
+ // Compiled MC command
637
+ if (doc.mc) {
638
+ md.appendText('\n')
639
+ md.appendMarkdown('**Compiles to:**\n')
640
+ md.appendCodeblock(doc.mc, 'mcfunction')
641
+ }
642
+
643
+ // Example
644
+ if (doc.example) {
645
+ md.appendMarkdown('**Example:**\n')
646
+ md.appendCodeblock(doc.example, 'redscript')
647
+ }
648
+
649
+ return md
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Selector documentation
654
+ // ---------------------------------------------------------------------------
655
+
656
+ const SELECTOR_DOCS: Record<string, { name: string; desc: string; tip?: string }> = {
657
+ '@s': { name: '@s — Self', desc: 'The entity that ran the current command (the executing entity).', tip: 'Always refers to exactly 1 entity.' },
658
+ '@a': { name: '@a — All Players', desc: 'All online players.', tip: 'Use `@a[limit=1]` to restrict to one player.' },
659
+ '@e': { name: '@e — All Entities', desc: 'All loaded entities (players + mobs + items + …).', tip: 'Usually combined with filters: `@e[type=minecraft:zombie,limit=5]`' },
660
+ '@p': { name: '@p — Nearest Player',desc: 'The single nearest player to the command origin.', tip: 'Exactly 1 player; errors if none are in range.' },
661
+ '@r': { name: '@r — Random Player', desc: 'A random online player.', tip: 'Use `@e[type=minecraft:player,sort=random,limit=1]` for full control.' },
662
+ '@n': { name: '@n — Nearest Entity',desc: 'The single nearest entity (including non-players).', tip: 'MC 1.21+ only.' },
663
+ }
664
+
665
+ /** Selector argument documentation (MC built-in selector arguments). */
666
+ const SELECTOR_ARG_DOCS: Record<string, { name: string; desc: string; example?: string }> = {
667
+ 'type': { name: 'type', desc: 'Filter by entity type.', example: 'type=minecraft:zombie' },
668
+ 'tag': { name: 'tag', desc: 'Filter by scoreboard tag. Use `tag=!name` to exclude.', example: 'tag=my_tag, tag=!excluded' },
669
+ 'name': { name: 'name', desc: 'Filter by entity custom name.', example: 'name="Steve"' },
670
+ 'team': { name: 'team', desc: 'Filter by team membership. Empty string = no team.', example: 'team=red, team=' },
671
+ 'scores': { name: 'scores', desc: 'Filter by scoreboard scores. Uses `{obj=range}` syntax.', example: 'scores={kills=1..}' },
672
+ 'nbt': { name: 'nbt', desc: 'Filter by NBT data match.', example: 'nbt={OnGround:1b}' },
673
+ 'predicate': { name: 'predicate', desc: 'Filter by datapack predicate.', example: 'predicate=my_pack:is_valid' },
674
+ 'gamemode': { name: 'gamemode', desc: 'Filter players by gamemode.', example: 'gamemode=survival, gamemode=!creative' },
675
+ 'distance': { name: 'distance', desc: 'Filter by distance from command origin. Supports ranges.', example: 'distance=..10, distance=5..20' },
676
+ 'level': { name: 'level', desc: 'Filter players by XP level.', example: 'level=10.., level=1..5' },
677
+ 'x_rotation': { name: 'x_rotation', desc: 'Filter by vertical head rotation (pitch). -90=up, 90=down.', example: 'x_rotation=-90..0' },
678
+ 'y_rotation': { name: 'y_rotation', desc: 'Filter by horizontal head rotation (yaw). South=0.', example: 'y_rotation=0..90' },
679
+ 'x': { name: 'x', desc: 'Override X coordinate for distance/volume calculations.', example: 'x=100' },
680
+ 'y': { name: 'y', desc: 'Override Y coordinate for distance/volume calculations.', example: 'y=64' },
681
+ 'z': { name: 'z', desc: 'Override Z coordinate for distance/volume calculations.', example: 'z=-200' },
682
+ 'dx': { name: 'dx', desc: 'X-size of selection box from x,y,z.', example: 'dx=10' },
683
+ 'dy': { name: 'dy', desc: 'Y-size of selection box from x,y,z.', example: 'dy=5' },
684
+ 'dz': { name: 'dz', desc: 'Z-size of selection box from x,y,z.', example: 'dz=10' },
685
+ 'limit': { name: 'limit', desc: 'Maximum number of entities to select.', example: 'limit=1, limit=5' },
686
+ 'sort': { name: 'sort', desc: 'Sort order: nearest, furthest, random, arbitrary.', example: 'sort=random' },
687
+ 'advancements':{ name: 'advancements', desc: 'Filter by advancement completion.', example: 'advancements={story/mine_diamond=true}' },
688
+ }
689
+
690
+ function formatSelectorHover(raw: string): vscode.MarkdownString {
691
+ const key = raw.replace(/\[.*/, '') as keyof typeof SELECTOR_DOCS
692
+ const info = SELECTOR_DOCS[key]
693
+ const md = new vscode.MarkdownString('', true)
694
+ if (info) {
695
+ md.appendMarkdown(`**${info.name}**\n\n`)
696
+ md.appendMarkdown(info.desc + '\n')
697
+ if (info.tip) md.appendMarkdown(`\n> 💡 ${info.tip}`)
698
+ } else {
699
+ md.appendMarkdown(`**Selector** \`${raw}\`\n\nEntity target selector.`)
700
+ }
701
+ return md
702
+ }
703
+
704
+ function formatSelectorArgHover(arg: string): vscode.MarkdownString | null {
705
+ const info = SELECTOR_ARG_DOCS[arg]
706
+ if (!info) return null
707
+ const md = new vscode.MarkdownString('', true)
708
+ md.appendMarkdown(`**${info.name}** (selector argument)\n\n`)
709
+ md.appendMarkdown(info.desc)
710
+ if (info.example) {
711
+ md.appendText('\n\n')
712
+ md.appendCodeblock(info.example, 'redscript')
713
+ }
714
+ return md
715
+ }
716
+
717
+ // ---------------------------------------------------------------------------
718
+ // JSDoc comment parser
719
+ // ---------------------------------------------------------------------------
720
+
721
+ /**
722
+ * Look backwards from `line` in `document` for a /** ... *\/ block.
723
+ * Returns the cleaned comment text, or null.
724
+ */
725
+ function findJsDocAbove(document: vscode.TextDocument, declLine: number): string | null {
726
+ // Walk up from the declaration line, skipping blank lines
727
+ let end = declLine - 1
728
+ while (end >= 0 && document.lineAt(end).text.trim() === '') end--
729
+ if (end < 0) return null
730
+
731
+ const endText = document.lineAt(end).text.trim()
732
+ if (!endText.endsWith('*/')) return null
733
+
734
+ // Find the opening /**
735
+ let start = end
736
+ while (start >= 0 && !document.lineAt(start).text.includes('/**')) start--
737
+ if (start < 0) return null
738
+
739
+ // Extract and clean comment lines
740
+ const lines: string[] = []
741
+ for (let i = start; i <= end; i++) {
742
+ let line = document.lineAt(i).text
743
+ .replace(/^\s*\/\*\*?\s?/, '') // remove leading /**
744
+ .replace(/\s*\*\/\s*$/, '') // remove trailing */
745
+ .replace(/^\s*\*\s?/, '') // remove leading * on middle lines
746
+ .trim()
747
+ if (line) lines.push(line)
748
+ }
749
+ return lines.length ? lines.join('\n') : null
750
+ }
751
+
752
+ /**
753
+ * Find the line where `fn <name>` is declared in the document.
754
+ */
755
+ function findFnDeclLine(document: vscode.TextDocument, name: string): number | null {
756
+ const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(`, 'm')
757
+ const text = document.getText()
758
+ const match = re.exec(text)
759
+ if (!match) return null
760
+ return document.positionAt(match.index).line
761
+ }
762
+
763
+ /**
764
+ * Extract the full function signature: fn name(params) -> ReturnType
765
+ */
766
+ function findFnSignature(document: vscode.TextDocument, name: string): string | null {
767
+ const text = document.getText()
768
+ // Match: fn name(params) or fn name(params) -> Type
769
+ const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))?`, 'm')
770
+ const match = re.exec(text)
771
+ if (!match) return null
772
+ const params = match[1].trim()
773
+ const returnType = match[2]
774
+ if (returnType) {
775
+ return `fn ${name}(${params}) -> ${returnType}`
776
+ }
777
+ return `fn ${name}(${params})`
778
+ }
779
+
780
+ function escapeRe(s: string): string {
781
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
782
+ }
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // Variable / let / const hover
786
+ // ---------------------------------------------------------------------------
787
+
788
+ interface VarDecl { name: string; type: string; kind: 'let' | 'const' | 'param' }
789
+
790
+ function findVarDecls(document: vscode.TextDocument): VarDecl[] {
791
+ const text = document.getText()
792
+ const decls: VarDecl[] = []
793
+
794
+ // Find let/const declarations
795
+ const letRe = /\b(let|const)\s+(\w+)\s*:\s*([A-Za-z_][A-Za-z0-9_\[\]]*)/g
796
+ let m: RegExpExecArray | null
797
+ while ((m = letRe.exec(text)) !== null) {
798
+ decls.push({ kind: m[1] as 'let' | 'const', name: m[2], type: m[3] })
799
+ }
800
+
801
+ return decls
802
+ }
803
+
804
+ interface FnParam { name: string; type: string; fnName: string; fnStartLine: number; fnEndLine: number }
805
+
806
+ /** Find all function parameters with their scope (function body range). */
807
+ function findFnParams(document: vscode.TextDocument): FnParam[] {
808
+ const text = document.getText()
809
+ const params: FnParam[] = []
810
+
811
+ // Match: fn name(param1: Type1, param2: Type2) { ... }
812
+ // Need to find the function body range to scope parameters correctly
813
+ const fnRe = /\bfn\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*\w+)?\s*\{/g
814
+ let fnMatch: RegExpExecArray | null
815
+
816
+ while ((fnMatch = fnRe.exec(text)) !== null) {
817
+ const fnName = fnMatch[1]
818
+ const paramsStr = fnMatch[2]
819
+ const fnStartOffset = fnMatch.index
820
+ const fnStartLine = document.positionAt(fnStartOffset).line
821
+
822
+ // Find matching closing brace for function body
823
+ const bodyStart = fnMatch.index + fnMatch[0].length - 1 // position of '{'
824
+ let braceCount = 1
825
+ let pos = bodyStart + 1
826
+ while (pos < text.length && braceCount > 0) {
827
+ if (text[pos] === '{') braceCount++
828
+ else if (text[pos] === '}') braceCount--
829
+ pos++
830
+ }
831
+ const fnEndLine = document.positionAt(pos).line
832
+
833
+ // Parse parameters: name: Type, name: Type = default
834
+ const paramRe = /(\w+)\s*:\s*([A-Za-z_][A-Za-z0-9_\[\]]*)/g
835
+ let paramMatch: RegExpExecArray | null
836
+ while ((paramMatch = paramRe.exec(paramsStr)) !== null) {
837
+ params.push({
838
+ name: paramMatch[1],
839
+ type: paramMatch[2],
840
+ fnName,
841
+ fnStartLine,
842
+ fnEndLine
843
+ })
844
+ }
845
+ }
846
+
847
+ return params
848
+ }
849
+
850
+ // ---------------------------------------------------------------------------
851
+ // Struct hover
852
+ // ---------------------------------------------------------------------------
853
+
854
+ interface StructField { name: string; type: string; line: number; doc?: string }
855
+ interface StructDecl { name: string; fields: StructField[]; line: number; doc?: string }
856
+
857
+ function findStructDecls(document: vscode.TextDocument): StructDecl[] {
858
+ const text = document.getText()
859
+ // Match: struct Name { field: Type, ... }
860
+ const structRe = /\bstruct\s+(\w+)\s*\{([^}]*)\}/gs
861
+ const decls: StructDecl[] = []
862
+ let m: RegExpExecArray | null
863
+
864
+ while ((m = structRe.exec(text)) !== null) {
865
+ const name = m[1]
866
+ const body = m[2]
867
+ const structLine = document.positionAt(m.index).line
868
+ const bodyStartOffset = m.index + m[0].indexOf('{') + 1
869
+
870
+ // Find JSDoc above struct
871
+ const structDoc = findJsDocAbove(document, structLine)
872
+
873
+ // Parse fields with their line numbers
874
+ const fieldRe = /\b(\w+)\s*:\s*([A-Za-z_][A-Za-z0-9_\[\]]*)/g
875
+ const fields: StructField[] = []
876
+ let fm: RegExpExecArray | null
877
+ while ((fm = fieldRe.exec(body)) !== null) {
878
+ const fieldOffset = bodyStartOffset + fm.index
879
+ const fieldLine = document.positionAt(fieldOffset).line
880
+ // Check for inline comment: field: Type, // comment
881
+ const lineText = document.lineAt(fieldLine).text
882
+ const inlineMatch = lineText.match(/\/\/\s*(.+)$/)
883
+ // Check for JSDoc/comment above field
884
+ const docAbove = findFieldDocAbove(document, fieldLine)
885
+ const fieldDoc = inlineMatch?.[1] || docAbove || undefined
886
+ fields.push({ name: fm[1], type: fm[2], line: fieldLine, doc: fieldDoc })
887
+ }
888
+
889
+ decls.push({ name, fields, line: structLine, doc: structDoc ?? undefined })
890
+ }
891
+ return decls
892
+ }
893
+
894
+ /** Find comment above a struct field (single // comment or /** block). */
895
+ function findFieldDocAbove(document: vscode.TextDocument, fieldLine: number): string | null {
896
+ if (fieldLine === 0) return null
897
+ const prevLine = document.lineAt(fieldLine - 1).text.trim()
898
+ // Check for // comment
899
+ if (prevLine.startsWith('//')) {
900
+ return prevLine.replace(/^\/\/\s*/, '')
901
+ }
902
+ // Check for /** */ on single line
903
+ const blockMatch = prevLine.match(/\/\*\*?\s*(.*?)\s*\*\//)
904
+ if (blockMatch) return blockMatch[1]
905
+ // Multi-line block comment
906
+ if (prevLine.endsWith('*/')) {
907
+ return findJsDocAbove(document, fieldLine)
908
+ }
909
+ return null
910
+ }
911
+
912
+ function formatStructHover(decl: StructDecl): vscode.MarkdownString {
913
+ const md = new vscode.MarkdownString('', true)
914
+ const lines = [`struct ${decl.name} {`]
915
+ for (const f of decl.fields) {
916
+ const comment = f.doc ? ` // ${f.doc}` : ''
917
+ lines.push(` ${f.name}: ${f.type},${comment}`)
918
+ }
919
+ lines.push('}')
920
+ md.appendCodeblock(lines.join('\n'), 'redscript')
921
+ if (decl.doc) {
922
+ md.appendText('\n')
923
+ md.appendMarkdown(decl.doc)
924
+ }
925
+ return md
926
+ }
927
+
928
+ function formatFieldHover(structName: string, field: StructField): vscode.MarkdownString {
929
+ const md = new vscode.MarkdownString('', true)
930
+ md.appendCodeblock(`(field) ${structName}.${field.name}: ${field.type}`, 'redscript')
931
+ if (field.doc) {
932
+ md.appendText('\n')
933
+ md.appendMarkdown(field.doc)
934
+ }
935
+ return md
936
+ }
937
+
938
+ // ---------------------------------------------------------------------------
939
+ // #mc_name hover
940
+ // ---------------------------------------------------------------------------
941
+
942
+ function formatMcNameHover(name: string): vscode.MarkdownString {
943
+ const md = new vscode.MarkdownString('', true)
944
+ md.appendCodeblock(`#${name}`, 'redscript')
945
+ md.appendMarkdown(`MC identifier \`${name}\`\n\nUsed as an objective, tag, team, or gamerule name. Compiles to the bare name \`${name}\` without quotes.`)
946
+ return md
947
+ }
948
+
949
+ // ---------------------------------------------------------------------------
950
+ // Hover provider
951
+ // ---------------------------------------------------------------------------
952
+
953
+ export function registerHoverProvider(context: vscode.ExtensionContext): void {
954
+ context.subscriptions.push(
955
+ vscode.languages.registerHoverProvider('redscript', {
956
+ provideHover(document, position) {
957
+ const line = document.lineAt(position.line).text
958
+
959
+ // ── #mc_name hover ──────────────────────────────────────
960
+ const mcRange = document.getWordRangeAtPosition(position, /#[a-zA-Z_][a-zA-Z0-9_]*/)
961
+ if (mcRange) {
962
+ const raw = document.getText(mcRange)
963
+ return new vscode.Hover(formatMcNameHover(raw.slice(1)), mcRange)
964
+ }
965
+
966
+ // ── Selector base hover (@a, @e, @s, etc.) ─────────────
967
+ // Match just the @x part (2 chars)
968
+ const baseSelectorRange = document.getWordRangeAtPosition(position, /@[aesprnAESPRN]/)
969
+ if (baseSelectorRange) {
970
+ const base = document.getText(baseSelectorRange)
971
+ return new vscode.Hover(formatSelectorHover(base), baseSelectorRange)
972
+ }
973
+
974
+ // ── Selector argument hover (inside [...]) ──────────────
975
+ // First get the word at cursor
976
+ const wordAtCursor = document.getWordRangeAtPosition(position, /[a-zA-Z_][a-zA-Z0-9_]*/)
977
+ if (wordAtCursor) {
978
+ const wordText = document.getText(wordAtCursor)
979
+ // Check if this word is a known selector argument AND is followed by '='
980
+ if (SELECTOR_ARG_DOCS[wordText]) {
981
+ const afterWord = line.slice(wordAtCursor.end.character).trimStart()
982
+ if (afterWord.startsWith('=')) {
983
+ // Verify we're actually inside selector brackets by looking backwards for @x[
984
+ const beforeWord = line.slice(0, wordAtCursor.start.character)
985
+ // Check if there's an unclosed @x[ before this word
986
+ const openBracket = beforeWord.lastIndexOf('[')
987
+ const closeBracket = beforeWord.lastIndexOf(']')
988
+ if (openBracket > closeBracket) {
989
+ // We're inside brackets, check if preceded by @x
990
+ const beforeBracket = beforeWord.slice(0, openBracket)
991
+ if (/@[aesprnAESPRN]\s*$/.test(beforeBracket)) {
992
+ const argDoc = formatSelectorArgHover(wordText)
993
+ if (argDoc) {
994
+ return new vscode.Hover(argDoc, wordAtCursor)
995
+ }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ const range = document.getWordRangeAtPosition(position, /[a-zA-Z_][a-zA-Z0-9_]*/)
1003
+ if (!range) return undefined
1004
+
1005
+ const word = document.getText(range)
1006
+
1007
+ // ── Discard/wildcard pattern _ ──────────────────────────
1008
+ if (word === '_') {
1009
+ const md = new vscode.MarkdownString('', true)
1010
+ md.appendCodeblock('_', 'redscript')
1011
+ md.appendMarkdown('**Wildcard pattern** (discard)\n\nMatches any value. Used in `match` expressions as a catch-all case, or to ignore unused values.')
1012
+ return new vscode.Hover(md, range)
1013
+ }
1014
+
1015
+ // ── Function parameter hover ────────────────────────────
1016
+ // Check if this word is a function parameter (must be inside that function's scope)
1017
+ const fnParams = findFnParams(document)
1018
+ const currentLine = position.line
1019
+ const param = fnParams.find(p =>
1020
+ p.name === word &&
1021
+ currentLine >= p.fnStartLine &&
1022
+ currentLine <= p.fnEndLine
1023
+ )
1024
+ if (param) {
1025
+ const md = new vscode.MarkdownString('', true)
1026
+ md.appendCodeblock(`(parameter) ${param.name}: ${param.type}`, 'redscript')
1027
+ return new vscode.Hover(md, range)
1028
+ }
1029
+
1030
+ // ── Variable / let / const hover ────────────────────────
1031
+ // Check if this word has a let/const declaration in the document
1032
+ const varDecls = findVarDecls(document)
1033
+ const varDecl = varDecls.find(v => v.name === word)
1034
+ if (varDecl) {
1035
+ const md = new vscode.MarkdownString('', true)
1036
+ md.appendCodeblock(`${varDecl.kind} ${varDecl.name}: ${varDecl.type}`, 'redscript')
1037
+ return new vscode.Hover(md, range)
1038
+ }
1039
+
1040
+ // ── Builtin function (only when used as a call, not variable name) ──
1041
+ // Only show builtin docs if the word is followed by '(' on the same line
1042
+ const afterWord = line.slice(range.end.character).trimStart()
1043
+ const isCall = afterWord.startsWith('(')
1044
+ if (isCall) {
1045
+ const builtin = BUILTINS[word]
1046
+ if (builtin) return new vscode.Hover(formatDoc(builtin), range)
1047
+ }
1048
+
1049
+ // ── Struct type hover ───────────────────────────────────
1050
+ const structDecls = findStructDecls(document)
1051
+ const structDecl = structDecls.find(s => s.name === word)
1052
+ if (structDecl) {
1053
+ return new vscode.Hover(formatStructHover(structDecl), range)
1054
+ }
1055
+
1056
+ // ── Member access: turret.tag ───────────────────────────
1057
+ // Check if word is preceded by '.' (member access)
1058
+ const charBefore = range.start.character > 0
1059
+ ? line.slice(range.start.character - 1, range.start.character)
1060
+ : ''
1061
+ if (charBefore === '.') {
1062
+ // Find the object name before the dot
1063
+ const beforeDot = line.slice(0, range.start.character - 1)
1064
+ const objMatch = beforeDot.match(/([A-Za-z_]\w*)$/)
1065
+ if (objMatch) {
1066
+ const objName = objMatch[1]
1067
+ // Find the type of the object from variable declarations
1068
+ const objVar = varDecls.find(v => v.name === objName)
1069
+ if (objVar) {
1070
+ const objStruct = structDecls.find(s => s.name === objVar.type)
1071
+ if (objStruct) {
1072
+ const field = objStruct.fields.find(f => f.name === word)
1073
+ if (field) {
1074
+ return new vscode.Hover(formatFieldHover(objStruct.name, field), range)
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ // ── Struct literal field key: { phase: value } ──────────
1082
+ // Check if word is followed by ':' (struct literal field)
1083
+ const afterWordTrimmed = afterWord
1084
+ if (afterWordTrimmed.startsWith(':')) {
1085
+ // Find the struct type from context: let x: StructType = { ... }
1086
+ const textBefore = document.getText(new vscode.Range(new vscode.Position(0, 0), position))
1087
+ const letMatch = textBefore.match(/let\s+\w+\s*:\s*(\w+)\s*=\s*\{[^}]*$/)
1088
+ const fnMatch = textBefore.match(/->\s*(\w+)\s*\{[^}]*return\s*\{[^}]*$/)
1089
+ const structType = letMatch?.[1] || fnMatch?.[1]
1090
+ if (structType) {
1091
+ const targetStruct = structDecls.find(s => s.name === structType)
1092
+ if (targetStruct) {
1093
+ const field = targetStruct.fields.find(f => f.name === word)
1094
+ if (field) {
1095
+ return new vscode.Hover(formatFieldHover(targetStruct.name, field), range)
1096
+ }
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ // ── User-defined function + JSDoc ───────────────────────
1102
+ // Only if used as a call
1103
+ if (isCall) {
1104
+ const declLine = findFnDeclLine(document, word)
1105
+ if (declLine !== null) {
1106
+ const md = new vscode.MarkdownString('', true)
1107
+ const jsdoc = findJsDocAbove(document, declLine)
1108
+ const sig = findFnSignature(document, word) || `fn ${word}(...)`
1109
+ md.appendCodeblock(sig, 'redscript')
1110
+ if (jsdoc) { md.appendText('\n'); md.appendMarkdown(jsdoc) }
1111
+ return new vscode.Hover(md, range)
1112
+ }
1113
+ }
1114
+
1115
+ return undefined
1116
+ }
1117
+ })
1118
+ )
1119
+ }
1120
+