@defold-typescript/types 0.51.0 → 0.53.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 (228) hide show
  1. package/api-availability.json +4 -58
  2. package/api-signatures.json +2804 -3172
  3. package/api-targets.json +15 -5
  4. package/examples/translations.json +19 -71
  5. package/generated/b2d.d.ts +39 -39
  6. package/generated/b2d_body.d.ts +11 -11
  7. package/generated/b2d_chain.d.ts +3 -3
  8. package/generated/b2d_fixture.d.ts +5 -5
  9. package/generated/b2d_joint.d.ts +29 -29
  10. package/generated/b2d_shape.d.ts +13 -13
  11. package/generated/b2d_world.d.ts +15 -15
  12. package/generated/buffer.d.ts +10 -11
  13. package/generated/bullet3d.d.ts +6 -6
  14. package/generated/bullet3d_collision_object.d.ts +10 -10
  15. package/generated/bullet3d_constraint.d.ts +173 -71
  16. package/generated/bullet3d_rigid_body.d.ts +3 -3
  17. package/generated/bullet3d_shape.d.ts +11 -11
  18. package/generated/bullet3d_world.d.ts +20 -20
  19. package/generated/camera.d.ts +4 -4
  20. package/generated/collectionfactory.d.ts +2 -3
  21. package/generated/collectionproxy.d.ts +5 -5
  22. package/generated/compute.d.ts +5 -5
  23. package/generated/crash.d.ts +7 -7
  24. package/generated/editor-vm/http.d.ts +13 -13
  25. package/generated/editor-vm/image.d.ts +4 -4
  26. package/generated/editor-vm/json.d.ts +1 -1
  27. package/generated/editor-vm/tilemap_tiles.d.ts +9 -9
  28. package/generated/editor-vm/zip.d.ts +4 -4
  29. package/generated/editor.d.ts +183 -183
  30. package/generated/factory.d.ts +2 -3
  31. package/generated/font.d.ts +4 -4
  32. package/generated/go.d.ts +122 -230
  33. package/generated/graphics.d.ts +23 -25
  34. package/generated/gui.d.ts +186 -423
  35. package/generated/http.d.ts +3 -3
  36. package/generated/image.d.ts +63 -11
  37. package/generated/json.d.ts +4 -4
  38. package/generated/kinds/gui-script.d.ts +2 -0
  39. package/generated/kinds/render-script.d.ts +14 -15
  40. package/generated/label.d.ts +5 -5
  41. package/generated/liveupdate.d.ts +7 -3
  42. package/generated/material.d.ts +49 -49
  43. package/generated/model.d.ts +46 -14
  44. package/generated/particlefx.d.ts +24 -5
  45. package/generated/physics.d.ts +64 -20
  46. package/generated/profiler.d.ts +3 -5
  47. package/generated/render-script-modules/b2d.d.ts +711 -0
  48. package/generated/render-script-modules/bullet3d.d.ts +73 -0
  49. package/generated/render-script-modules/collectionfactory.d.ts +24 -0
  50. package/generated/render-script-modules/collectionproxy.d.ts +39 -0
  51. package/generated/render-script-modules/factory.d.ts +24 -0
  52. package/generated/render-script-modules/go.d.ts +434 -0
  53. package/generated/render-script-modules/label.d.ts +105 -0
  54. package/generated/render-script-modules/model.d.ts +121 -0
  55. package/generated/render-script-modules/particlefx.d.ts +59 -0
  56. package/generated/render-script-modules/physics.d.ts +333 -0
  57. package/generated/render-script-modules/resource.d.ts +1232 -0
  58. package/generated/render-script-modules/sound.d.ts +227 -0
  59. package/generated/render-script-modules/sprite.d.ts +68 -0
  60. package/generated/render-script-modules/tilemap.d.ts +43 -0
  61. package/generated/render.d.ts +172 -47
  62. package/generated/resource.d.ts +81 -61
  63. package/generated/socket.d.ts +18 -23
  64. package/generated/sound.d.ts +6 -6
  65. package/generated/sprite.d.ts +51 -3
  66. package/generated/sys.d.ts +87 -24
  67. package/generated/tilemap.d.ts +3 -3
  68. package/generated/timer.d.ts +3 -3
  69. package/generated/versions/defold-1.12.4/b2d.d.ts +2 -2
  70. package/generated/versions/defold-1.12.4/b2d_body.d.ts +2 -2
  71. package/generated/versions/defold-1.12.4/buffer.d.ts +8 -8
  72. package/generated/versions/defold-1.12.4/go.d.ts +117 -345
  73. package/generated/versions/defold-1.12.4/graphics.d.ts +2 -1
  74. package/generated/versions/defold-1.12.4/gui.d.ts +169 -532
  75. package/generated/versions/defold-1.12.4/http.d.ts +2 -2
  76. package/generated/versions/defold-1.12.4/image.d.ts +82 -2
  77. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  78. package/generated/versions/defold-1.12.4/label.d.ts +1 -1
  79. package/generated/versions/defold-1.12.4/liveupdate.d.ts +4 -0
  80. package/generated/versions/defold-1.12.4/model.d.ts +42 -10
  81. package/generated/versions/defold-1.12.4/particlefx.d.ts +23 -2
  82. package/generated/versions/defold-1.12.4/physics.d.ts +46 -2
  83. package/generated/versions/defold-1.12.4/render.d.ts +83 -7
  84. package/generated/versions/defold-1.12.4/resource.d.ts +50 -17
  85. package/generated/versions/defold-1.12.4/socket.d.ts +12 -12
  86. package/generated/versions/defold-1.12.4/sound.d.ts +1 -1
  87. package/generated/versions/defold-1.12.4/sprite.d.ts +59 -2
  88. package/generated/versions/defold-1.12.4/sys.d.ts +103 -8
  89. package/generated/versions/defold-1.12.4/tilemap.d.ts +1 -1
  90. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -24
  91. package/generated/versions/defold-1.12.4/window.d.ts +3 -4
  92. package/generated/versions/defold-1.13.0/b2d.d.ts +2 -2
  93. package/generated/versions/defold-1.13.0/b2d_body.d.ts +2 -2
  94. package/generated/versions/defold-1.13.0/b2d_chain.d.ts +1 -1
  95. package/generated/versions/defold-1.13.0/b2d_joint.d.ts +1 -1
  96. package/generated/versions/defold-1.13.0/b2d_shape.d.ts +1 -1
  97. package/generated/versions/defold-1.13.0/buffer.d.ts +8 -8
  98. package/generated/versions/defold-1.13.0/camera.d.ts +1 -1
  99. package/generated/versions/defold-1.13.0/go.d.ts +117 -372
  100. package/generated/versions/defold-1.13.0/graphics.d.ts +2 -1
  101. package/generated/versions/defold-1.13.0/gui.d.ts +169 -532
  102. package/generated/versions/defold-1.13.0/http.d.ts +2 -2
  103. package/generated/versions/defold-1.13.0/image.d.ts +82 -2
  104. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  105. package/generated/versions/defold-1.13.0/label.d.ts +1 -1
  106. package/generated/versions/defold-1.13.0/liveupdate.d.ts +4 -0
  107. package/generated/versions/defold-1.13.0/material.d.ts +3 -1
  108. package/generated/versions/defold-1.13.0/model.d.ts +42 -10
  109. package/generated/versions/defold-1.13.0/particlefx.d.ts +23 -2
  110. package/generated/versions/defold-1.13.0/physics.d.ts +46 -2
  111. package/generated/versions/defold-1.13.0/render.d.ts +147 -9
  112. package/generated/versions/defold-1.13.0/resource.d.ts +50 -17
  113. package/generated/versions/defold-1.13.0/socket.d.ts +12 -12
  114. package/generated/versions/defold-1.13.0/sound.d.ts +1 -1
  115. package/generated/versions/defold-1.13.0/sprite.d.ts +59 -2
  116. package/generated/versions/defold-1.13.0/sys.d.ts +103 -8
  117. package/generated/versions/defold-1.13.0/tilemap.d.ts +1 -1
  118. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -24
  119. package/generated/versions/defold-1.13.0/window.d.ts +3 -4
  120. package/generated/versions/defold-1.13.1/b2d.d.ts +2 -2
  121. package/generated/versions/defold-1.13.1/b2d_body.d.ts +2 -2
  122. package/generated/versions/defold-1.13.1/b2d_chain.d.ts +1 -1
  123. package/generated/versions/defold-1.13.1/b2d_joint.d.ts +1 -1
  124. package/generated/versions/defold-1.13.1/b2d_shape.d.ts +1 -1
  125. package/generated/versions/defold-1.13.1/buffer.d.ts +8 -8
  126. package/generated/versions/defold-1.13.1/camera.d.ts +1 -1
  127. package/generated/versions/defold-1.13.1/go.d.ts +117 -372
  128. package/generated/versions/defold-1.13.1/graphics.d.ts +2 -1
  129. package/generated/versions/defold-1.13.1/gui.d.ts +169 -548
  130. package/generated/versions/defold-1.13.1/http.d.ts +2 -2
  131. package/generated/versions/defold-1.13.1/image.d.ts +82 -2
  132. package/generated/versions/defold-1.13.1/index.d.ts +2 -0
  133. package/generated/versions/defold-1.13.1/label.d.ts +1 -1
  134. package/generated/versions/defold-1.13.1/liveupdate.d.ts +4 -0
  135. package/generated/versions/defold-1.13.1/material.d.ts +3 -1
  136. package/generated/versions/defold-1.13.1/model.d.ts +42 -10
  137. package/generated/versions/defold-1.13.1/particlefx.d.ts +23 -2
  138. package/generated/versions/defold-1.13.1/physics.d.ts +46 -2
  139. package/generated/versions/defold-1.13.1/render.d.ts +147 -9
  140. package/generated/versions/defold-1.13.1/resource.d.ts +50 -17
  141. package/generated/versions/defold-1.13.1/socket.d.ts +12 -12
  142. package/generated/versions/defold-1.13.1/sound.d.ts +1 -1
  143. package/generated/versions/defold-1.13.1/sprite.d.ts +59 -2
  144. package/generated/versions/defold-1.13.1/sys.d.ts +103 -8
  145. package/generated/versions/defold-1.13.1/tilemap.d.ts +1 -1
  146. package/generated/versions/defold-1.13.1/vmath.d.ts +0 -24
  147. package/generated/versions/defold-1.13.1/window.d.ts +3 -4
  148. package/generated/versions/defold-1.13.2/b2d.d.ts +39 -39
  149. package/generated/versions/defold-1.13.2/b2d_body.d.ts +11 -11
  150. package/generated/versions/defold-1.13.2/b2d_chain.d.ts +3 -3
  151. package/generated/versions/defold-1.13.2/b2d_fixture.d.ts +5 -5
  152. package/generated/versions/defold-1.13.2/b2d_joint.d.ts +29 -29
  153. package/generated/versions/defold-1.13.2/b2d_shape.d.ts +13 -13
  154. package/generated/versions/defold-1.13.2/b2d_world.d.ts +15 -15
  155. package/generated/versions/defold-1.13.2/buffer.d.ts +10 -11
  156. package/generated/versions/defold-1.13.2/bullet3d.d.ts +6 -6
  157. package/generated/versions/defold-1.13.2/bullet3d_collision_object.d.ts +10 -10
  158. package/generated/versions/defold-1.13.2/bullet3d_constraint.d.ts +173 -71
  159. package/generated/versions/defold-1.13.2/bullet3d_rigid_body.d.ts +3 -3
  160. package/generated/versions/defold-1.13.2/bullet3d_shape.d.ts +11 -11
  161. package/generated/versions/defold-1.13.2/bullet3d_world.d.ts +20 -20
  162. package/generated/versions/defold-1.13.2/camera.d.ts +4 -4
  163. package/generated/versions/defold-1.13.2/collectionfactory.d.ts +2 -3
  164. package/generated/versions/defold-1.13.2/collectionproxy.d.ts +5 -5
  165. package/generated/versions/defold-1.13.2/compute.d.ts +5 -5
  166. package/generated/versions/defold-1.13.2/crash.d.ts +7 -7
  167. package/generated/versions/defold-1.13.2/editor-vm/http.d.ts +13 -13
  168. package/generated/versions/defold-1.13.2/editor-vm/image.d.ts +4 -4
  169. package/generated/versions/defold-1.13.2/editor-vm/json.d.ts +1 -1
  170. package/generated/versions/defold-1.13.2/editor-vm/tilemap_tiles.d.ts +9 -9
  171. package/generated/versions/defold-1.13.2/editor-vm/zip.d.ts +4 -4
  172. package/generated/versions/defold-1.13.2/editor.d.ts +183 -183
  173. package/generated/versions/defold-1.13.2/factory.d.ts +2 -3
  174. package/generated/versions/defold-1.13.2/font.d.ts +4 -4
  175. package/generated/versions/defold-1.13.2/go.d.ts +122 -230
  176. package/generated/versions/defold-1.13.2/graphics.d.ts +22 -24
  177. package/generated/versions/defold-1.13.2/gui.d.ts +186 -423
  178. package/generated/versions/defold-1.13.2/http.d.ts +3 -3
  179. package/generated/versions/defold-1.13.2/image.d.ts +63 -11
  180. package/generated/versions/defold-1.13.2/index.d.ts +2 -0
  181. package/generated/versions/defold-1.13.2/json.d.ts +4 -4
  182. package/generated/versions/defold-1.13.2/label.d.ts +5 -5
  183. package/generated/versions/defold-1.13.2/liveupdate.d.ts +7 -3
  184. package/generated/versions/defold-1.13.2/material.d.ts +49 -49
  185. package/generated/versions/defold-1.13.2/model.d.ts +46 -14
  186. package/generated/versions/defold-1.13.2/particlefx.d.ts +24 -5
  187. package/generated/versions/defold-1.13.2/physics.d.ts +64 -20
  188. package/generated/versions/defold-1.13.2/profiler.d.ts +3 -5
  189. package/generated/versions/defold-1.13.2/render.d.ts +161 -46
  190. package/generated/versions/defold-1.13.2/resource.d.ts +81 -61
  191. package/generated/versions/defold-1.13.2/socket.d.ts +18 -23
  192. package/generated/versions/defold-1.13.2/sound.d.ts +6 -6
  193. package/generated/versions/defold-1.13.2/sprite.d.ts +51 -3
  194. package/generated/versions/defold-1.13.2/sys.d.ts +87 -24
  195. package/generated/versions/defold-1.13.2/tilemap.d.ts +3 -3
  196. package/generated/versions/defold-1.13.2/timer.d.ts +3 -3
  197. package/generated/versions/defold-1.13.2/vmath.d.ts +0 -25
  198. package/generated/versions/defold-1.13.2/window.d.ts +8 -10
  199. package/generated/vmath.d.ts +0 -25
  200. package/generated/window.d.ts +8 -10
  201. package/identity-vocabulary.json +86 -34
  202. package/index.d.ts +2 -0
  203. package/package.json +3 -1
  204. package/scripts/generate-api-availability.ts +10 -24
  205. package/scripts/generate-api-signatures.ts +75 -14
  206. package/scripts/materialize-version.ts +48 -7
  207. package/scripts/regen.ts +214 -19
  208. package/scripts/shared-handle-methods.ts +3 -2
  209. package/scripts/signature-store-fs.ts +1 -0
  210. package/scripts/target-version.ts +26 -0
  211. package/src/api-doc.ts +34 -0
  212. package/src/api-identity.ts +32 -1
  213. package/src/component-properties.d.ts +96 -7
  214. package/src/core-types.ts +71 -28
  215. package/src/doc-corrections.ts +62 -5
  216. package/src/emit-dts.ts +1423 -401
  217. package/src/engine-globals.d.ts +5 -1
  218. package/src/engine-type-name.ts +33 -0
  219. package/src/go-overloads.d.ts +74 -19
  220. package/src/gui-overloads.d.ts +489 -0
  221. package/src/gui-texture-removal.d.ts +41 -0
  222. package/src/index.ts +3 -0
  223. package/src/lifecycle.ts +32 -23
  224. package/src/msg-overloads.d.ts +25 -13
  225. package/src/signature-store.ts +50 -0
  226. package/src/socket-types.d.ts +3 -3
  227. package/src/timers.d.ts +2 -1
  228. package/src/vmath-overloads.d.ts +26 -0
package/src/emit-dts.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
2
- import { type ApiSymbolIdentity, normalizedFunctionSignature } from "./api-availability";
2
+ import {
3
+ type ApiSymbolIdentity,
4
+ type Box2dBackend,
5
+ normalizedFunctionSignature,
6
+ } from "./api-availability";
3
7
  import type {
4
8
  ApiConstant,
5
9
  ApiFunction,
@@ -11,7 +15,7 @@ import type {
11
15
  ApiTypedef,
12
16
  ApiVariable,
13
17
  } from "./api-doc";
14
- import { DEFOLD_TYPE_MAP } from "./core-types";
18
+ import { DEFOLD_TYPE_MAP, SOCKET_HANDLE_NAMES, type SocketHandleReceiver } from "./core-types";
15
19
  import { correctionPins, pinMatches, propertyCorrectionPins } from "./correction-pins";
16
20
  import {
17
21
  type DocCommentParts,
@@ -22,6 +26,7 @@ import {
22
26
  summaryFor,
23
27
  } from "./doc-comment";
24
28
  import { applyMessageOverlays, parseMessagesDoc } from "./emit-messages";
29
+ import { engineTypeName } from "./engine-type-name";
25
30
  import type { TranslationStore } from "./example-store";
26
31
  import { lookupExampleSegments, translateProseFences } from "./example-store";
27
32
  import {
@@ -36,6 +41,7 @@ import {
36
41
  type LualsPosition,
37
42
  mapLualsExpression,
38
43
  needsArrayParens,
44
+ splitTopLevel,
39
45
  } from "./luals-type-expr";
40
46
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
41
47
 
@@ -82,12 +88,20 @@ export interface EmitOptions {
82
88
  // bindings (a ref-doc target), where every entry the ref-doc omits is emitted.
83
89
  registeredConstants?: ReadonlySet<string>;
84
90
  // The `RETURN_TYPE_CORRECTIONS` and `STRUCT_MEMBER_ADDITIONS` keys whose binding
85
- // pin holds in the target's vendored engine source. A binding-pinned entry
86
- // applies only when its key is here, so a surface with no vendored bindings
87
- // (absent) applies none of them.
91
+ // pin holds in the target's vendored engine source, and the
92
+ // `PROPERTY_KEY_RANGES` keys whose `engine` pins all hold there. A pinned
93
+ // entry applies only when its key is here, so a surface with no vendored
94
+ // bindings (absent) applies none of them.
88
95
  bindingEvidence?: ReadonlySet<string>;
96
+ // The 1-based parameter slots, keyed by function FQN, that the target's engine
97
+ // bindings let a call omit but raise on when passed nil. Each optional one
98
+ // splits its function into a call without the slot and a call requiring it,
99
+ // so a possibly-undefined value is a compile error. Absent for a surface with
100
+ // no vendored bindings (a ref-doc target), which keeps every `?`.
101
+ nilRaisingSlots?: ReadonlyMap<string, readonly number[]>;
89
102
  // The functions a target's skip rules removed from the module, read only to
90
- // type an `OVERLOAD_SLOT_ALIASES` slot.
103
+ // type an `OVERLOAD_SLOT_ALIASES` slot and to resolve a
104
+ // `CONSTANT_SLOT_RESOLUTIONS` borrow from their docs.
91
105
  withheldFunctions?: readonly ApiFunction[];
92
106
  }
93
107
 
@@ -179,14 +193,12 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
179
193
  // runtime-owned passthrough: the table is filled by user code at runtime, never
180
194
  // by Defold's API contract. factory.create/collectionfactory.create `properties`
181
195
  // are spawn-time overrides keyed by the spawned object's own script-property
182
- // names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
183
- // payload (mitigated for authored code by isMessage/onMessage). No static shape
184
- // exists, so `Record<string | number, unknown>` is faithful, not a loss.
185
- // collectionfactory.create curates the instance-id layer of `properties` as a
186
- // mapping; each per-instance property table stays that faithful `Record`.
196
+ // names. No static shape exists, so `Record<string | number, unknown>` is
197
+ // faithful, not a loss. collectionfactory.create curates the instance-id layer
198
+ // of `properties` as a mapping; each per-instance property table stays that
199
+ // faithful `Record`.
187
200
  "factory.create",
188
201
  "collectionfactory.create",
189
- "on_message",
190
202
  // engine-formatted blob: the engine produces the value at runtime and no
191
203
  // field list is documented, so the whole-slot `Record` is faithful, not a
192
204
  // loss. crash.get_backtrace is "table containing the backtrace",
@@ -259,6 +271,11 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
259
271
  "go.get",
260
272
  "go.set",
261
273
  "go.property",
274
+ // gui-overloads.d.ts closes the node property names and types each value per
275
+ // name, with a hash form for material constants.
276
+ "gui.animate",
277
+ "gui.get",
278
+ "gui.set",
262
279
  // msg-overloads.d.ts supplies the typed msg.post shape.
263
280
  "msg.post",
264
281
  // msg-overloads.d.ts supplies the typed msg.url shape (0 / 1-string / 3-arg
@@ -277,6 +294,7 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
277
294
  "vmath.slerp",
278
295
  "vmath.mul_per_elem",
279
296
  "vmath.normalize",
297
+ "vmath.dot",
280
298
  // sys-overloads.d.ts supplies the disabling form and the form that takes the
281
299
  // cooldown the engine requires when enabling.
282
300
  "sys.set_engine_throttle",
@@ -334,13 +352,10 @@ export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: stri
334
352
  ]);
335
353
 
336
354
  // FQN-keyed return-type overrides for functions whose doc `returnvalues` are
337
- // empty yet the engine returns a value. `gui.get`'s doc lists no return, but it
338
- // yields the property value (a vmath.vector4 or a number); `unknown` is the
339
- // honest, ts-defold-matching shape. Per-FQN, not a blanket "empty returnvalues
340
- // -> unknown" rule: `gui.set` also has empty returnvalues and `void` is correct
341
- // there.
355
+ // empty yet the engine returns a value. Per-FQN, not a blanket "empty
356
+ // returnvalues -> unknown" rule: most functions with empty returnvalues return
357
+ // nothing, and `void` is correct there.
342
358
  export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
343
- ["gui.get", "unknown"],
344
359
  // Upstream documents the returned transaction step in prose only — its
345
360
  // `returnvalues` is empty while every sibling `editor.tx.*` builder declares
346
361
  // one, and `editor.transact()` takes exactly that.
@@ -459,8 +474,8 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
459
474
  slot: "code",
460
475
  },
461
476
  ],
462
- // 1.13.2 declares `render_predicate`, whose TYPEDEF aliases `userdata`, and
463
- // that still emits `number`.
477
+ // 1.13.2 retypes the return to `render_predicate`, whose TYPEDEF aliases
478
+ // `userdata`; its type mapping emits the same brand.
464
479
  [
465
480
  "render.predicate",
466
481
  {
@@ -538,6 +553,20 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
538
553
  binding: tilemapEarlyNil("TileMap_GetTiles"),
539
554
  },
540
555
  ],
556
+ [
557
+ "bullet3d.shape.get_shape",
558
+ {
559
+ ts: "bullet3d.shape.Definition | { type: typeof bullet3d.shape.SHAPE_TYPE_MESH }",
560
+ upstream: ["bullet3d.shape.definition"],
561
+ reason:
562
+ "script_bullet3d_shape.cpp:Shape_GetShape sets only `type` for a triangle mesh, which no shape definition arm matches",
563
+ binding: {
564
+ file: "gamesys/src/gamesys/scripts/bullet3d/script_bullet3d_shape.cpp",
565
+ cFunction: "Shape_GetShape",
566
+ contains: "case TRIANGLE_MESH_SHAPE_PROXYTYPE: case GIMPACT_SHAPE_PROXYTYPE: break;",
567
+ },
568
+ },
569
+ ],
541
570
  [
542
571
  "collectionproxy.set_collection",
543
572
  {
@@ -631,6 +660,36 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
631
660
  },
632
661
  ] as const,
633
662
  ),
663
+ [
664
+ "gui.get_xanchor",
665
+ {
666
+ ts: "Exclude<gui.Anchor, typeof gui.ANCHOR_BOTTOM | typeof gui.ANCHOR_TOP>",
667
+ upstream: ["constant"],
668
+ retypedUpstream: [["gui.ANCHOR"]],
669
+ reason:
670
+ "gui_script.cpp:LuaGetXAnchor pushes GetNodeXAnchor, an x anchor: XANCHOR_NONE, XANCHOR_LEFT, or XANCHOR_RIGHT",
671
+ binding: {
672
+ file: "gui/src/gui_script.cpp",
673
+ cFunction: "LuaGetXAnchor",
674
+ contains: "lua_pushnumber(L, GetNodeXAnchor(scene, hnode));",
675
+ },
676
+ },
677
+ ],
678
+ [
679
+ "gui.get_yanchor",
680
+ {
681
+ ts: "Exclude<gui.Anchor, typeof gui.ANCHOR_LEFT | typeof gui.ANCHOR_RIGHT>",
682
+ upstream: ["constant"],
683
+ retypedUpstream: [["gui.ANCHOR"]],
684
+ reason:
685
+ "gui_script.cpp:LuaGetYAnchor pushes GetNodeYAnchor, a y anchor: YANCHOR_NONE, YANCHOR_TOP, or YANCHOR_BOTTOM",
686
+ binding: {
687
+ file: "gui/src/gui_script.cpp",
688
+ cFunction: "LuaGetYAnchor",
689
+ contains: "lua_pushnumber(L, GetNodeYAnchor(scene, hnode));",
690
+ },
691
+ },
692
+ ],
634
693
  ]);
635
694
 
636
695
  // A parameter whose declared `types` disagree with what the same element's
@@ -638,23 +697,29 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
638
697
  // target by resource name, a texture unit, a Lua sequence where `table` is
639
698
  // declared, a hash the binding refuses as a string. Admission rule: only a slot
640
699
  // that evidence contradicts; a slot that is merely imprecise is not a candidate.
641
- // The correction edits the mapped union rather than replacing it: `adds` appends
642
- // what the slot also takes and `removes` drops members the binding raises on, so
643
- // the URL address alias, array curation and constant mapping stay live, and
644
- // optionality stays with `isDocOptional`. Each entry records the upstream tokens
645
- // it contradicts, plus any later spelling of the same defect in
646
- // `retypedUpstream`, and applies only to a slot still declaring one of them;
647
- // `param-correction-provenance.test.ts` pins them against every retained
648
- // vendored ref-doc. Once no retained target still declares any of them the
649
- // entry is deleted, never re-pinned.
650
- export interface ParamTypeCorrection {
651
- readonly adds?: string;
652
- readonly removes?: readonly string[];
700
+ // A correction either replaces the mapped type exactly with `ts`, or edits the
701
+ // mapped union: `adds` appends what the slot also takes and `removes` drops
702
+ // members the binding raises on. Union edits keep the URL address alias, array
703
+ // curation and constant mapping live; optionality stays with `isDocOptional`.
704
+ // Each entry records the upstream tokens it contradicts, plus any later spelling
705
+ // of the same defect in `retypedUpstream`, and applies only to a slot still
706
+ // declaring one of them. An entry with a `binding` also requires its exact
707
+ // `EmitOptions.bindingEvidence` key. `param-correction-provenance.test.ts` pins
708
+ // the ref-doc tokens against every retained target; once no target declares a
709
+ // pin, the entry is deleted rather than re-pinned.
710
+ interface ParamTypeCorrectionBase {
653
711
  readonly upstream: readonly string[];
654
712
  readonly retypedUpstream?: readonly (readonly string[])[];
655
713
  readonly reason: string;
714
+ readonly binding?: BindingPin;
656
715
  }
657
716
 
717
+ export type ParamTypeCorrection = ParamTypeCorrectionBase &
718
+ (
719
+ | { readonly ts: string; readonly adds?: undefined; readonly removes?: undefined }
720
+ | { readonly ts?: undefined; readonly adds?: string; readonly removes?: readonly string[] }
721
+ );
722
+
658
723
  const CAMERA_ADDRESS_REASON =
659
724
  "render_script_camera.cpp:CheckRenderCamera resolves anything but a number handle through ResolveURL, which takes a string or a hash";
660
725
 
@@ -688,6 +753,58 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
688
753
  string,
689
754
  ParamTypeCorrection
690
755
  >([
756
+ [
757
+ "gui.set_xanchor:param:anchor",
758
+ {
759
+ ts: "Exclude<gui.Anchor, typeof gui.ANCHOR_BOTTOM | typeof gui.ANCHOR_TOP>",
760
+ upstream: ["constant"],
761
+ retypedUpstream: [["gui.ANCHOR"]],
762
+ reason:
763
+ "gui_script.cpp:LuaSetXAnchor rejects every value except XANCHOR_NONE, XANCHOR_LEFT, and XANCHOR_RIGHT",
764
+ binding: {
765
+ file: "gui/src/gui_script.cpp",
766
+ cFunction: "LuaSetXAnchor",
767
+ contains: "anchor != XANCHOR_NONE && anchor != XANCHOR_LEFT && anchor != XANCHOR_RIGHT",
768
+ },
769
+ },
770
+ ],
771
+ [
772
+ "gui.set_yanchor:param:anchor",
773
+ {
774
+ ts: "Exclude<gui.Anchor, typeof gui.ANCHOR_LEFT | typeof gui.ANCHOR_RIGHT>",
775
+ upstream: ["constant"],
776
+ retypedUpstream: [["gui.ANCHOR"]],
777
+ reason:
778
+ "gui_script.cpp:LuaSetYAnchor rejects every value except YANCHOR_NONE, YANCHOR_TOP, and YANCHOR_BOTTOM",
779
+ binding: {
780
+ file: "gui/src/gui_script.cpp",
781
+ cFunction: "LuaSetYAnchor",
782
+ contains: "anchor != YANCHOR_NONE && anchor != YANCHOR_TOP && anchor != YANCHOR_BOTTOM",
783
+ },
784
+ },
785
+ ],
786
+ [
787
+ "go.animate:param:playback",
788
+ {
789
+ ts: "go.Playback",
790
+ upstream: [
791
+ "go.PLAYBACK_ONCE_FORWARD",
792
+ "go.PLAYBACK_ONCE_BACKWARD",
793
+ "go.PLAYBACK_ONCE_PINGPONG",
794
+ "go.PLAYBACK_LOOP_FORWARD",
795
+ "go.PLAYBACK_LOOP_BACKWARD",
796
+ "go.PLAYBACK_LOOP_PINGPONG",
797
+ ],
798
+ retypedUpstream: [["go.PLAYBACK"]],
799
+ reason:
800
+ "gameobject_script.cpp:Script_Animate accepts every playback value below PLAYBACK_COUNT, including PLAYBACK_NONE",
801
+ binding: {
802
+ file: "gameobject/src/gameobject/gameobject_script.cpp",
803
+ cFunction: "Script_Animate",
804
+ contains: "if (playback >= PLAYBACK_COUNT)",
805
+ },
806
+ },
807
+ ],
691
808
  ...CAMERA_SLOT_FUNCTIONS.map(
692
809
  (name) =>
693
810
  [
@@ -757,6 +874,35 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
757
874
  },
758
875
  ] as const,
759
876
  ),
877
+ ...(
878
+ [
879
+ ["clone_tree", "LuaCloneTree"],
880
+ ["get_tree", "LuaGetTree"],
881
+ ] as const
882
+ ).map(
883
+ ([name, cFunction]) =>
884
+ [
885
+ `gui.${name}:param:node`,
886
+ {
887
+ adds: "undefined",
888
+ upstream: ["node"],
889
+ reason: `gui_script.cpp:${cFunction} pushes its result table before testing argument 1 with lua_isnil, so nil selects the whole scene and an absent argument reaches LuaCheckNodeInternal and raises`,
890
+ },
891
+ ] as const,
892
+ ),
893
+ ...(["get_slider_motor", "set_slider_motor"] as const).map(
894
+ (name) =>
895
+ [
896
+ `bullet3d.constraint.${name}:param:motor`,
897
+ {
898
+ adds: '"linear" | "angular"',
899
+ removes: ["string"],
900
+ upstream: ["string"],
901
+ reason:
902
+ "script_bullet3d_constraint.cpp:CheckSliderMotorAxis raises \"motor must be 'linear' or 'angular'\" on any other string",
903
+ },
904
+ ] as const,
905
+ ),
760
906
  [
761
907
  "sound.get_group_name:param:group",
762
908
  {
@@ -835,9 +981,10 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
835
981
  "render.disable_texture:param:binding",
836
982
  {
837
983
  adds: "number",
984
+ removes: ['Opaque<"texture">'],
838
985
  upstream: ["texture", "string", "hash"],
839
986
  reason:
840
- '"texture binding, either by texture unit, string or hash"; example `render.disable_texture(0)`',
987
+ "render_script.cpp:RenderScript_DisableTexture reads a number as a texture unit with lua_isnumber and anything else with CheckHashOrString; a texture handle is a number, so it is read as a unit",
841
988
  },
842
989
  ],
843
990
  [
@@ -855,11 +1002,42 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
855
1002
  adds: 'Opaque<"render_predicate">',
856
1003
  removes: ["number"],
857
1004
  upstream: ["number"],
858
- retypedUpstream: [["render_predicate"]],
859
1005
  reason:
860
- "render_script.cpp:RenderScript_Draw reads the predicate with RenderScriptPredicate_Check, the userdata render.predicate returns; 1.13.2's `render_predicate` still emits `number`",
1006
+ "render_script.cpp:RenderScript_Draw reads the predicate with RenderScriptPredicate_Check, the userdata render.predicate returns; 1.13.2 types the slot `render_predicate`, whose type mapping is that brand",
861
1007
  },
862
1008
  ],
1009
+ ...(["enable_state", "disable_state"] as const).map(
1010
+ (name) =>
1011
+ [
1012
+ `render.${name}:param:state`,
1013
+ {
1014
+ adds: "Exclude<graphics.State, typeof graphics.STATE_ALPHA_TEST_SUPPORTED | typeof graphics.STATE_SCISSOR_TEST>",
1015
+ removes: ["graphics.State"],
1016
+ upstream: ["graphics.STATE"],
1017
+ reason: `render_script.cpp:RenderScript_${name === "enable_state" ? "Enable" : "Disable"}State raises "Invalid state" on any state but the six it lists, so graphics.STATE_SCISSOR_TEST and graphics.STATE_ALPHA_TEST_SUPPORTED are rejected`,
1018
+ },
1019
+ ] as const,
1020
+ ),
1021
+ ...(
1022
+ [
1023
+ "source_factor_color",
1024
+ "destination_factor_color",
1025
+ "source_factor_alpha",
1026
+ "destination_factor_alpha",
1027
+ ] as const
1028
+ ).map(
1029
+ (slot) =>
1030
+ [
1031
+ `render.set_blend_func_separate:param:${slot}`,
1032
+ {
1033
+ adds: "graphics.BlendFactor",
1034
+ removes: ["number"],
1035
+ upstream: ["number"],
1036
+ reason:
1037
+ 'render_script.cpp:RenderScript_SetBlendFuncSeparate raises "Invalid blend factor" on any value CheckBlendFactor does not list',
1038
+ },
1039
+ ] as const,
1040
+ ),
863
1041
  [
864
1042
  "go.delete:param:id",
865
1043
  {
@@ -878,15 +1056,6 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
878
1056
  reason: 'sys.load_buffer example `buffer.get_bytes(my_buffer, "data")`',
879
1057
  },
880
1058
  ],
881
- [
882
- "gui.set:param:value",
883
- {
884
- adds: "Matrix4 | Hash",
885
- upstream: ["number", "vector4", "vector3", "quaternion"],
886
- reason:
887
- 'example `gui.set(node, "light_matrix", vmath.matrix4())` ("matrix4 is also supported"); example `gui.set(msg.url(), "textures", atlas_id, {key = "runtime_texture"})` passes the hash `resource.create_atlas` returns',
888
- },
889
- ],
890
1059
  [
891
1060
  "gui.set_rotation:param:rotation",
892
1061
  {
@@ -918,8 +1087,8 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
918
1087
  [
919
1088
  "window.set_dim_mode:param:mode",
920
1089
  {
921
- removes: ["window.DIMMING"],
922
- adds: "window.DimModeStateSettable",
1090
+ removes: ["window.Dimming"],
1091
+ adds: "Exclude<window.Dimming, typeof window.DIMMING_UNKNOWN>",
923
1092
  upstream: ["window.DIMMING"],
924
1093
  reason:
925
1094
  'script_window.cpp:SetDimMode raises "The dim mode specified is not supported." for anything but DIMMING_ON and DIMMING_OFF',
@@ -1021,16 +1190,70 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
1021
1190
  // saying it, or when the phrase stops stating `first-last`. Curated rather than
1022
1191
  // matched on a trailing `N`, so each new upstream placeholder is a deliberate
1023
1192
  // addition.
1193
+ //
1194
+ // `engine.last` replaces `last` on a target whose vendored engine source shows
1195
+ // every one of `engine.pins` (the `EmitOptions.bindingEvidence` keys); every
1196
+ // other surface, including one generated from a ref-doc alone, keeps the range
1197
+ // `evidence` states.
1024
1198
  export interface PropertyKeyRange {
1025
1199
  readonly prefix: string;
1026
1200
  readonly first: number;
1027
1201
  readonly last: number;
1028
1202
  readonly evidence: string;
1203
+ readonly engine?: { readonly last: number; readonly pins: readonly EngineSourcePin[] };
1029
1204
  }
1030
1205
 
1031
- // Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS.
1206
+ // A token snippet the target's vendored engine evidence must contain. With
1207
+ // `cFunction` it is matched in that function's body, without it anywhere in
1208
+ // the file.
1209
+ export interface EngineSourcePin {
1210
+ // Relative to the target's vendored `engine/` tree, as `BindingPin.file` is.
1211
+ readonly file: string;
1212
+ readonly cFunction?: string;
1213
+ readonly contains: string;
1214
+ }
1215
+
1216
+ const MODEL_TEXTURE_LOOP =
1217
+ "for (uint32_t i = 0; i < dmRender::RenderObject::MAX_TEXTURE_COUNT; ++i) { if (params.m_PropertyId == PROP_TEXTURE[i])";
1218
+
1219
+ // Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS; a key
1220
+ // with an `engine` field is also an `EmitOptions.bindingEvidence` key.
1032
1221
  export const PROPERTY_KEY_RANGES: ReadonlyMap<string, PropertyKeyRange> = new Map([
1033
- ["model.textureN", { prefix: "texture", first: 0, last: 7, evidence: "textureN where N is 0-7" }],
1222
+ [
1223
+ "model.textureN",
1224
+ {
1225
+ prefix: "texture",
1226
+ first: 0,
1227
+ last: 7,
1228
+ evidence: "textureN where N is 0-7",
1229
+ engine: {
1230
+ last: 15,
1231
+ pins: [
1232
+ {
1233
+ file: "render/src/dmsdk/render/render.h",
1234
+ contains: "static const uint32_t MAX_TEXTURE_COUNT = 16;",
1235
+ },
1236
+ {
1237
+ file: "gamesys/src/gamesys/gamesys.cpp",
1238
+ contains: `const dmhash_t PROP_TEXTURE[dmRender::RenderObject::MAX_TEXTURE_COUNT] = { ${Array.from(
1239
+ { length: 16 },
1240
+ (_, unit) => `dmHashString64("texture${unit}"),`,
1241
+ ).join(" ")} };`,
1242
+ },
1243
+ {
1244
+ file: "gamesys/src/gamesys/components/comp_model.cpp",
1245
+ cFunction: "CompModelGetProperty",
1246
+ contains: MODEL_TEXTURE_LOOP,
1247
+ },
1248
+ {
1249
+ file: "gamesys/src/gamesys/components/comp_model.cpp",
1250
+ cFunction: "CompModelSetProperty",
1251
+ contains: MODEL_TEXTURE_LOOP,
1252
+ },
1253
+ ],
1254
+ },
1255
+ },
1256
+ ],
1034
1257
  ]);
1035
1258
 
1036
1259
  // A parameter the ref-doc metadata marks required while the same element's prose
@@ -1095,6 +1318,10 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
1095
1318
  "render.set_render_target:param:render_target",
1096
1319
  "render_script.cpp:RenderScript_SetRenderTarget sets the default target when lua_gettop(L) == 0",
1097
1320
  ],
1321
+ [
1322
+ "buffer.get_bytes:param:stream_name",
1323
+ "script_buffer.cpp:GetBytes reads only argument 1 with CheckBufferUnpack and returns every byte of the buffer",
1324
+ ],
1098
1325
  ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
1099
1326
  ...(
1100
1327
  [
@@ -1237,13 +1464,6 @@ export const REQUIRED_SLOT_CORRECTIONS: ReadonlyMap<string, RequiredSlotCorrecti
1237
1464
  evidence: "gui_script.cpp:LuaSetParent reads a node from argument 2 unless lua_isnil(L, 2)",
1238
1465
  },
1239
1466
  ],
1240
- [
1241
- "gui.set:param:value",
1242
- {
1243
- nil: true,
1244
- evidence: "gui_script.cpp:LuaSet converts argument 3 with LuaToVar unless lua_isnil(L, 3)",
1245
- },
1246
- ],
1247
1467
  [
1248
1468
  "window.set_listener:param:callback",
1249
1469
  {
@@ -1333,6 +1553,335 @@ function firstSlotAlternative(
1333
1553
  return original === null ? null : { ...prepared, original };
1334
1554
  }
1335
1555
 
1556
+ // The calls a function splits into at its optional nil-raising slots
1557
+ // (`EmitOptions.nilRaisingSlots`): one ending before each such slot, then one
1558
+ // taking them all, every such slot before an arm's end declared required. A
1559
+ // required slot, or one a rest parameter folds in, splits nothing.
1560
+ function nilRaisingArms(
1561
+ prepared: PreparedFunction,
1562
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]>,
1563
+ ): PreparedFunction[] {
1564
+ const { name, parameters } = prepared.original;
1565
+ const firstVararg = parameters.findIndex(isVarargParameter);
1566
+ const positional = firstVararg === -1 ? parameters.length : firstVararg;
1567
+ const split = [...new Set(nilRaisingSlots.get(name) ?? [])]
1568
+ .map((slot) => slot - 1)
1569
+ .filter((index) => {
1570
+ const p = parameters[index];
1571
+ return index < positional && p !== undefined && isDocOptional(p, name);
1572
+ })
1573
+ .sort((a, b) => a - b);
1574
+ if (split.length === 0) return [prepared];
1575
+ const shorter = split.map((end, k) => ({
1576
+ ...prepared,
1577
+ original: { ...prepared.original, parameters: parameters.slice(0, end) },
1578
+ required: new Set(split.slice(0, k)),
1579
+ }));
1580
+ return [...shorter, { ...prepared, required: new Set(split) }];
1581
+ }
1582
+
1583
+ // Table fields a binding reads as required only when `argument` is non-nil,
1584
+ // keyed by FQN. A curated function splits into a call whose `table` slot requires
1585
+ // `fields`, with `argument` as declared, then a call whose `argument` is nil, so
1586
+ // a possibly-nil argument must carry them.
1587
+ export interface PresenceRequiredFields {
1588
+ readonly argument: string;
1589
+ readonly table: string;
1590
+ readonly fields: readonly string[];
1591
+ readonly reason: string;
1592
+ }
1593
+
1594
+ const FRAME_B_FIELDS = ["frame_b_position", "frame_b_rotation"] as const;
1595
+
1596
+ export const PRESENCE_REQUIRED_FIELDS: ReadonlyMap<string, PresenceRequiredFields> = new Map<
1597
+ string,
1598
+ PresenceRequiredFields
1599
+ >([
1600
+ [
1601
+ "bullet3d.constraint.create_point_to_point",
1602
+ {
1603
+ argument: "body_b",
1604
+ table: "params",
1605
+ fields: ["pivot_b"],
1606
+ reason:
1607
+ "script_bullet3d_constraint.cpp:Constraint_CreatePointToPoint reads pivot_b as required when input.m_BodyB != 0",
1608
+ },
1609
+ ],
1610
+ ...(
1611
+ [
1612
+ ["create_hinge", "Constraint_CreateHinge"],
1613
+ ["create_cone_twist", "Constraint_CreateConeTwist"],
1614
+ ["create_generic_6dof", "Constraint_CreateGeneric6Dof"],
1615
+ ["create_slider", "Constraint_CreateSlider"],
1616
+ ] as const
1617
+ ).map(
1618
+ ([name, cFunction]) =>
1619
+ [
1620
+ `bullet3d.constraint.${name}`,
1621
+ {
1622
+ argument: "body_b",
1623
+ table: "params",
1624
+ fields: FRAME_B_FIELDS,
1625
+ reason: `script_bullet3d_constraint.cpp:${cFunction} reads frame B as required under if (input.m_BodyB)`,
1626
+ },
1627
+ ] as const,
1628
+ ),
1629
+ ]);
1630
+
1631
+ // The calls PRESENCE_REQUIRED_FIELDS splits `prepared` into: the declared one
1632
+ // with its fields required, then the one whose argument is nil.
1633
+ function presenceArms(
1634
+ prepared: PreparedFunction,
1635
+ structs: readonly ApiStruct[],
1636
+ ): PreparedFunction[] {
1637
+ const { name, parameters } = prepared.original;
1638
+ const entry = PRESENCE_REQUIRED_FIELDS.get(name);
1639
+ if (entry === undefined) return [prepared];
1640
+ const argument = parameters.findIndex((p) => p.name === entry.argument);
1641
+ const table = parameters.findIndex((p) => p.name === entry.table);
1642
+ const struct = structs.find((s) => parameters[table]?.types.includes(s.name));
1643
+ if (argument === -1 || struct === undefined) {
1644
+ throw new Error(
1645
+ `presence-required fields ${name}: no ${entry.argument} parameter or ${entry.table} table`,
1646
+ );
1647
+ }
1648
+ const missing = entry.fields.filter((field) => !struct.members.some((m) => m.name === field));
1649
+ if (missing.length > 0) {
1650
+ throw new Error(`presence-required fields ${name}: ${struct.name} lacks ${missing.join(", ")}`);
1651
+ }
1652
+ const picked = entry.fields.map((field) => `"${field}"`).join(" | ");
1653
+ const requireFields = (type: string): string => {
1654
+ const operand = /^[\w$.]+$/.test(type) ? type : `(${type})`;
1655
+ return `${operand} & Required<Pick<${operand}, ${picked}>>`;
1656
+ };
1657
+ const strict: PreparedFunction = { ...prepared, retype: new Map([[table, requireFields]]) };
1658
+ // Declared required, the nil argument emits as `undefined` alone rather than
1659
+ // gaining the `| undefined` of an interior optional slot.
1660
+ const absent: PreparedFunction = {
1661
+ ...prepared,
1662
+ required: new Set([...(prepared.required ?? []), argument]),
1663
+ retype: new Map([[argument, () => "undefined"]]),
1664
+ };
1665
+ return [strict, absent];
1666
+ }
1667
+
1668
+ // A rule an engine binding enforces on one parameter that no ref-doc token
1669
+ // states, keyed `<fqn>:param:<name>`. Each rewrites the slot's mapped type, so
1670
+ // one entry holds on every target whatever token spelling produced that type.
1671
+ export type EngineSlotRule =
1672
+ // `E[]` becomes `[E, ...E[]]`: the binding raises on an empty list.
1673
+ | { readonly kind: "non-empty"; readonly reason: string }
1674
+ // The constants the binding raises on leave the slot's type.
1675
+ | { readonly kind: "excludes"; readonly constants: readonly string[]; readonly reason: string }
1676
+ // With `flag` true the table must carry `fields`, which the binding then reads
1677
+ // without a default.
1678
+ | {
1679
+ readonly kind: "flag-requires";
1680
+ readonly flag: string;
1681
+ readonly fields: readonly string[];
1682
+ readonly reason: string;
1683
+ }
1684
+ // In each call that requires `slot`, this slot is required too and its
1685
+ // top-level union loses `drops`.
1686
+ | {
1687
+ readonly kind: "alongside";
1688
+ readonly slot: string;
1689
+ readonly drops: readonly string[];
1690
+ readonly reason: string;
1691
+ };
1692
+
1693
+ export const ENGINE_SLOT_RULES: ReadonlyMap<string, EngineSlotRule> = new Map<
1694
+ string,
1695
+ EngineSlotRule
1696
+ >([
1697
+ [
1698
+ "buffer.create:param:declaration",
1699
+ {
1700
+ kind: "non-empty",
1701
+ reason:
1702
+ 'script_buffer.cpp:Create raises "You must specify at least one stream declaration" when the list holds fewer than one entry',
1703
+ },
1704
+ ],
1705
+ [
1706
+ "buffer.set_metadata:param:values",
1707
+ {
1708
+ kind: "non-empty",
1709
+ reason:
1710
+ 'script_buffer.cpp:SetMetadata raises "invalid metadata" when lua_objlen of values is 0',
1711
+ },
1712
+ ],
1713
+ [
1714
+ "buffer.set_metadata:param:value_type",
1715
+ {
1716
+ kind: "excludes",
1717
+ constants: ["buffer.VALUE_TYPE_INT64", "buffer.VALUE_TYPE_UINT64"],
1718
+ reason:
1719
+ 'script_buffer.cpp:SetMetadata raises "64 bit integer metadata are not supported." on VALUE_TYPE_UINT64 and VALUE_TYPE_INT64',
1720
+ },
1721
+ ],
1722
+ [
1723
+ "resource.create_sound_data:param:options",
1724
+ {
1725
+ kind: "flag-requires",
1726
+ flag: "partial",
1727
+ fields: ["filesize"],
1728
+ reason:
1729
+ "script_resource.cpp:CreateSoundData reads filesize with the no-default CheckFieldValue<int> when partial is true, and CheckValue<int> raises on a non-number",
1730
+ },
1731
+ ],
1732
+ [
1733
+ "go.delete:param:id",
1734
+ {
1735
+ kind: "alongside",
1736
+ slot: "recursive",
1737
+ drops: ["boolean"],
1738
+ reason:
1739
+ "gameobject_script.cpp:Script_Delete raises on a boolean id with a second argument, and on a nil id once recursive is popped",
1740
+ },
1741
+ ],
1742
+ ]);
1743
+
1744
+ // `prepared` with `rewrite` applied after any rewrite it already makes at `index`.
1745
+ function withRetype(
1746
+ prepared: PreparedFunction,
1747
+ index: number,
1748
+ rewrite: (type: string) => string,
1749
+ ): PreparedFunction {
1750
+ const retype = new Map(prepared.retype);
1751
+ const prior = retype.get(index);
1752
+ retype.set(index, prior === undefined ? rewrite : (type) => rewrite(prior(type)));
1753
+ return { ...prepared, retype };
1754
+ }
1755
+
1756
+ // `E[]` as `[E, ...E[]]`; the element keeps its own parentheses.
1757
+ function nonEmptyList(type: string, key: string): string {
1758
+ const element = type.endsWith("[]") ? type.slice(0, -2) : "";
1759
+ const single =
1760
+ splitTopLevel(type, "|").length === 1 &&
1761
+ splitTopLevel(type, "&").length === 1 &&
1762
+ !isFunctionTypeText(type);
1763
+ if (element === "" || !single) {
1764
+ throw new Error(`engine slot rule ${key}: ${type} is not one array`);
1765
+ }
1766
+ return `[${element}, ...${element}[]]`;
1767
+ }
1768
+
1769
+ function withoutMembers(type: string, drops: readonly string[], key: string): string {
1770
+ const members = splitTopLevel(type, "|").map((member) => member.trim());
1771
+ const missing = drops.filter((drop) => !members.includes(drop));
1772
+ if (missing.length > 0) {
1773
+ throw new Error(`engine slot rule ${key}: ${type} has no ${missing.join(", ")} to drop`);
1774
+ }
1775
+ return members.filter((member) => !drops.includes(member)).join(" | ");
1776
+ }
1777
+
1778
+ // The field names a table slot declares: its STRUCT's members, else the fields
1779
+ // parsed from its own doc.
1780
+ function slotFieldNames(parameter: ApiParameter, structs: readonly ApiStruct[]): string[] {
1781
+ const struct = structs.find((s) => parameter.types.includes(s.name));
1782
+ if (struct !== undefined) return struct.members.map((member) => member.name);
1783
+ const fields = parameter.fields ?? parseTableFields(parameter.doc) ?? [];
1784
+ return fields.map((field) => field.name);
1785
+ }
1786
+
1787
+ function engineSlotRuleArms(
1788
+ prepared: PreparedFunction,
1789
+ arms: readonly PreparedFunction[],
1790
+ key: string,
1791
+ rule: EngineSlotRule,
1792
+ index: number,
1793
+ structs: readonly ApiStruct[],
1794
+ bindingsRead: boolean,
1795
+ ): PreparedFunction[] {
1796
+ const { name, parameters } = prepared.original;
1797
+ const takes = (arm: PreparedFunction): boolean => index < arm.original.parameters.length;
1798
+ const rewriteEach = (rewrite: (type: string) => string): PreparedFunction[] =>
1799
+ arms.map((arm) => (takes(arm) ? withRetype(arm, index, rewrite) : arm));
1800
+ switch (rule.kind) {
1801
+ case "non-empty":
1802
+ return rewriteEach((type) => nonEmptyList(type, key));
1803
+ case "excludes": {
1804
+ const excluded = rule.constants.map((constant) => `typeof ${constant}`).join(" | ");
1805
+ return rewriteEach((type) => `Exclude<${type}, ${excluded}>`);
1806
+ }
1807
+ case "flag-requires": {
1808
+ const declared = slotFieldNames(parameters[index] as ApiParameter, structs);
1809
+ const missing = [rule.flag, ...rule.fields].filter((field) => !declared.includes(field));
1810
+ if (missing.length > 0) {
1811
+ throw new Error(`engine slot rule ${key}: the table declares no ${missing.join(", ")}`);
1812
+ }
1813
+ const picked = rule.fields.map((field) => `"${field}"`).join(" | ");
1814
+ return rewriteEach((type) => {
1815
+ const operand =
1816
+ splitTopLevel(type, "|").length > 1 || isFunctionTypeText(type) ? `(${type})` : type;
1817
+ return `${operand} & ({ ${rule.flag}?: false } | ({ ${rule.flag}: true } & Required<Pick<${type}, ${picked}>>))`;
1818
+ });
1819
+ }
1820
+ case "alongside": {
1821
+ const partner = parameters.findIndex((p) => p.name === rule.slot);
1822
+ const partnerParameter = parameters[partner];
1823
+ const requires = (arm: PreparedFunction): boolean =>
1824
+ partnerParameter !== undefined &&
1825
+ partner < arm.original.parameters.length &&
1826
+ (arm.required?.has(partner) === true || !isDocOptional(partnerParameter, name));
1827
+ if (!arms.some(requires)) {
1828
+ // Without the target's nil-raising slots no call was split out to
1829
+ // require the partner, so there is no binding to have stopped marking it.
1830
+ if (!bindingsRead) return [...arms];
1831
+ throw new Error(
1832
+ partnerParameter === undefined
1833
+ ? `engine slot rule ${key}: ${name} has no ${rule.slot} parameter`
1834
+ : `engine slot rule ${key}: no call of ${name} requires ${rule.slot}`,
1835
+ );
1836
+ }
1837
+ return arms.map((arm) =>
1838
+ requires(arm)
1839
+ ? {
1840
+ ...withRetype(arm, index, (type) => withoutMembers(type, rule.drops, key)),
1841
+ required: new Set([...(arm.required ?? []), index]),
1842
+ }
1843
+ : arm,
1844
+ );
1845
+ }
1846
+ }
1847
+ }
1848
+
1849
+ // `arms` with every ENGINE_SLOT_RULES entry keyed to `prepared` applied.
1850
+ // `bindingsRead` says the emit was given the target's nil-raising slots.
1851
+ function applyEngineSlotRules(
1852
+ prepared: PreparedFunction,
1853
+ arms: readonly PreparedFunction[],
1854
+ structs: readonly ApiStruct[],
1855
+ bindingsRead: boolean,
1856
+ ): PreparedFunction[] {
1857
+ const { name, parameters } = prepared.original;
1858
+ const prefix = `${name}:param:`;
1859
+ let ruled = [...arms];
1860
+ for (const [key, rule] of ENGINE_SLOT_RULES) {
1861
+ if (!key.startsWith(prefix)) continue;
1862
+ const slot = key.slice(prefix.length);
1863
+ const index = parameters.findIndex((p) => p.name === slot);
1864
+ if (index === -1) {
1865
+ throw new Error(`engine slot rule ${key}: ${name} has no ${slot} parameter`);
1866
+ }
1867
+ ruled = engineSlotRuleArms(prepared, ruled, key, rule, index, structs, bindingsRead);
1868
+ }
1869
+ return ruled;
1870
+ }
1871
+
1872
+ // Every call a function declares, in order: its nil-raising arms, each split
1873
+ // at its presence-required fields, then narrowed by its engine slot rules.
1874
+ function callArms(
1875
+ prepared: PreparedFunction,
1876
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]> | undefined,
1877
+ structs: readonly ApiStruct[],
1878
+ ): PreparedFunction[] {
1879
+ const arms = nilRaisingArms(prepared, nilRaisingSlots ?? new Map()).flatMap((arm) =>
1880
+ presenceArms(arm, structs),
1881
+ );
1882
+ return applyEngineSlotRules(prepared, arms, structs, nilRaisingSlots !== undefined);
1883
+ }
1884
+
1336
1885
  export type ConstantSlotResolution =
1337
1886
  | { readonly borrow: string }
1338
1887
  | { readonly family: string; readonly evidence: string }
@@ -1347,8 +1896,6 @@ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResoluti
1347
1896
  string,
1348
1897
  ConstantSlotResolution
1349
1898
  >([
1350
- ["gui.get:param:property", { borrow: "gui.animate:param:property" }],
1351
- ["gui.set:param:property", { borrow: "gui.animate:param:property" }],
1352
1899
  ["gui.cancel_animations:param:property", { borrow: "gui.animate:param:property" }],
1353
1900
  [
1354
1901
  "buffer.set_metadata:param:value_type",
@@ -1366,7 +1913,7 @@ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResoluti
1366
1913
  ],
1367
1914
  ]);
1368
1915
 
1369
- export interface ConstantUnionAlias {
1916
+ export interface ConstantUnionNamedRow {
1370
1917
  // The namespace the alias is declared in. It is the namespace the union
1371
1918
  // *means*, which is not always the one its constants live in: the three
1372
1919
  // buffer keys `render.clear` accepts are `graphics.BUFFER_TYPE_*` constants,
@@ -1377,11 +1924,22 @@ export interface ConstantUnionAlias {
1377
1924
  readonly members: readonly string[];
1378
1925
  }
1379
1926
 
1380
- // A documented constant union gets a public name so a caller can spell the type
1381
- // their own wrapper takes. Identity is the *member set*, never a name prefix:
1382
- // `gui.ANCHOR_*` splits into two rows because the x and y slots accept
1383
- // different thirds of it, and `graphics.BUFFER_TYPE_*` splits because
1384
- // `render.clear` accepts three of the six.
1927
+ // A union that is a strict subset of a named row in the same home. It declares
1928
+ // nothing: each slot spells it `Exclude<<home>.<parent>, ...>`, so one namespace
1929
+ // never carries two names for overlapping sets.
1930
+ export interface ConstantUnionSubsetRow {
1931
+ readonly home: string;
1932
+ readonly parent: string;
1933
+ readonly members: readonly string[];
1934
+ }
1935
+
1936
+ export type ConstantUnionAlias = ConstantUnionNamedRow | ConstantUnionSubsetRow;
1937
+
1938
+ // A documented constant union gets a public spelling so a caller can write the
1939
+ // type their own wrapper takes. Identity is the *member set*, never a name
1940
+ // prefix: the x and y anchor slots accept different thirds of `gui.ANCHOR_*`,
1941
+ // so each is its own subset row of `gui.ANCHOR`, and `graphics.BUFFER_TYPE_*`
1942
+ // splits because `render.clear` accepts three of the six.
1385
1943
  //
1386
1944
  // The table is the only thing the emitter reads, and every name in it has been
1387
1945
  // reviewed: a longest-common-prefix rule proposes, a human disposes. A union
@@ -1391,7 +1949,7 @@ export interface ConstantUnionAlias {
1391
1949
  export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1392
1950
  {
1393
1951
  home: "buffer",
1394
- name: "ValueType",
1952
+ name: "VALUE_TYPE",
1395
1953
  members: [
1396
1954
  "buffer.VALUE_TYPE_FLOAT32",
1397
1955
  "buffer.VALUE_TYPE_INT16",
@@ -1406,7 +1964,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1406
1964
  },
1407
1965
  {
1408
1966
  home: "collectionfactory",
1409
- name: "Status",
1967
+ name: "STATUS",
1410
1968
  members: [
1411
1969
  "collectionfactory.STATUS_UNLOADED",
1412
1970
  "collectionfactory.STATUS_LOADING",
@@ -1415,12 +1973,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1415
1973
  },
1416
1974
  {
1417
1975
  home: "factory",
1418
- name: "Status",
1976
+ name: "STATUS",
1419
1977
  members: ["factory.STATUS_UNLOADED", "factory.STATUS_LOADING", "factory.STATUS_LOADED"],
1420
1978
  },
1421
1979
  {
1422
1980
  home: "go",
1423
- name: "Easing",
1981
+ name: "EASING",
1424
1982
  members: [
1425
1983
  "go.EASING_INBACK",
1426
1984
  "go.EASING_INBOUNCE",
@@ -1467,7 +2025,20 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1467
2025
  },
1468
2026
  {
1469
2027
  home: "go",
1470
- name: "Playback",
2028
+ name: "PLAYBACK",
2029
+ members: [
2030
+ "go.PLAYBACK_LOOP_BACKWARD",
2031
+ "go.PLAYBACK_LOOP_FORWARD",
2032
+ "go.PLAYBACK_LOOP_PINGPONG",
2033
+ "go.PLAYBACK_NONE",
2034
+ "go.PLAYBACK_ONCE_BACKWARD",
2035
+ "go.PLAYBACK_ONCE_FORWARD",
2036
+ "go.PLAYBACK_ONCE_PINGPONG",
2037
+ ],
2038
+ },
2039
+ {
2040
+ home: "go",
2041
+ parent: "PLAYBACK",
1471
2042
  members: [
1472
2043
  "go.PLAYBACK_ONCE_FORWARD",
1473
2044
  "go.PLAYBACK_ONCE_BACKWARD",
@@ -1477,9 +2048,32 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1477
2048
  "go.PLAYBACK_LOOP_PINGPONG",
1478
2049
  ],
1479
2050
  },
2051
+ // The factors render_script.cpp:CheckBlendFactor accepts, in its order. Every
2052
+ // target defines the constants; only the newer ones name the union.
2053
+ {
2054
+ home: "graphics",
2055
+ name: "BLEND_FACTOR",
2056
+ members: [
2057
+ "graphics.BLEND_FACTOR_ZERO",
2058
+ "graphics.BLEND_FACTOR_ONE",
2059
+ "graphics.BLEND_FACTOR_SRC_COLOR",
2060
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_COLOR",
2061
+ "graphics.BLEND_FACTOR_DST_COLOR",
2062
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_COLOR",
2063
+ "graphics.BLEND_FACTOR_SRC_ALPHA",
2064
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA",
2065
+ "graphics.BLEND_FACTOR_DST_ALPHA",
2066
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_ALPHA",
2067
+ "graphics.BLEND_FACTOR_SRC_ALPHA_SATURATE",
2068
+ "graphics.BLEND_FACTOR_CONSTANT_COLOR",
2069
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR",
2070
+ "graphics.BLEND_FACTOR_CONSTANT_ALPHA",
2071
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA",
2072
+ ],
2073
+ },
1480
2074
  {
1481
2075
  home: "graphics",
1482
- name: "BufferType",
2076
+ name: "BUFFER_TYPE",
1483
2077
  members: [
1484
2078
  "graphics.BUFFER_TYPE_COLOR0_BIT",
1485
2079
  "graphics.BUFFER_TYPE_COLOR1_BIT",
@@ -1491,7 +2085,22 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1491
2085
  },
1492
2086
  {
1493
2087
  home: "graphics",
1494
- name: "State",
2088
+ name: "STATE",
2089
+ members: [
2090
+ "graphics.STATE_ALPHA_TEST",
2091
+ "graphics.STATE_ALPHA_TEST_SUPPORTED",
2092
+ "graphics.STATE_BLEND",
2093
+ "graphics.STATE_CULL_FACE",
2094
+ "graphics.STATE_DEPTH_TEST",
2095
+ "graphics.STATE_POLYGON_OFFSET_FILL",
2096
+ "graphics.STATE_SCISSOR_TEST",
2097
+ "graphics.STATE_STENCIL_TEST",
2098
+ ],
2099
+ },
2100
+ // The six render_script.cpp:RenderScript_EnableState accepts.
2101
+ {
2102
+ home: "graphics",
2103
+ parent: "STATE",
1495
2104
  members: [
1496
2105
  "graphics.STATE_DEPTH_TEST",
1497
2106
  "graphics.STATE_STENCIL_TEST",
@@ -1503,12 +2112,33 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1503
2112
  },
1504
2113
  {
1505
2114
  home: "gui",
1506
- name: "AdjustMode",
2115
+ name: "ADJUST",
1507
2116
  members: ["gui.ADJUST_FIT", "gui.ADJUST_ZOOM", "gui.ADJUST_STRETCH"],
1508
2117
  },
1509
2118
  {
1510
2119
  home: "gui",
1511
- name: "BlendMode",
2120
+ name: "ANCHOR",
2121
+ members: [
2122
+ "gui.ANCHOR_BOTTOM",
2123
+ "gui.ANCHOR_LEFT",
2124
+ "gui.ANCHOR_NONE",
2125
+ "gui.ANCHOR_RIGHT",
2126
+ "gui.ANCHOR_TOP",
2127
+ ],
2128
+ },
2129
+ {
2130
+ home: "gui",
2131
+ parent: "ANCHOR",
2132
+ members: ["gui.ANCHOR_NONE", "gui.ANCHOR_LEFT", "gui.ANCHOR_RIGHT"],
2133
+ },
2134
+ {
2135
+ home: "gui",
2136
+ parent: "ANCHOR",
2137
+ members: ["gui.ANCHOR_NONE", "gui.ANCHOR_TOP", "gui.ANCHOR_BOTTOM"],
2138
+ },
2139
+ {
2140
+ home: "gui",
2141
+ name: "BLEND",
1512
2142
  members: [
1513
2143
  "gui.BLEND_ALPHA",
1514
2144
  "gui.BLEND_ADD",
@@ -1519,12 +2149,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1519
2149
  },
1520
2150
  {
1521
2151
  home: "gui",
1522
- name: "ClippingMode",
2152
+ name: "CLIPPING_MODE",
1523
2153
  members: ["gui.CLIPPING_MODE_NONE", "gui.CLIPPING_MODE_STENCIL"],
1524
2154
  },
1525
2155
  {
1526
2156
  home: "gui",
1527
- name: "Easing",
2157
+ name: "EASING",
1528
2158
  members: [
1529
2159
  "gui.EASING_INBACK",
1530
2160
  "gui.EASING_INBOUNCE",
@@ -1571,7 +2201,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1571
2201
  },
1572
2202
  {
1573
2203
  home: "gui",
1574
- name: "KeyboardType",
2204
+ name: "KEYBOARD_TYPE",
1575
2205
  members: [
1576
2206
  "gui.KEYBOARD_TYPE_DEFAULT",
1577
2207
  "gui.KEYBOARD_TYPE_EMAIL",
@@ -1581,7 +2211,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1581
2211
  },
1582
2212
  {
1583
2213
  home: "gui",
1584
- name: "NodeType",
2214
+ name: "TYPE",
1585
2215
  members: [
1586
2216
  "gui.TYPE_BOX",
1587
2217
  "gui.TYPE_TEXT",
@@ -1592,12 +2222,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1592
2222
  },
1593
2223
  {
1594
2224
  home: "gui",
1595
- name: "PieBounds",
2225
+ name: "PIEBOUNDS",
1596
2226
  members: ["gui.PIEBOUNDS_RECTANGLE", "gui.PIEBOUNDS_ELLIPSE"],
1597
2227
  },
1598
2228
  {
1599
2229
  home: "gui",
1600
- name: "Pivot",
2230
+ name: "PIVOT",
1601
2231
  members: [
1602
2232
  "gui.PIVOT_CENTER",
1603
2233
  "gui.PIVOT_N",
@@ -1612,7 +2242,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1612
2242
  },
1613
2243
  {
1614
2244
  home: "gui",
1615
- name: "Playback",
2245
+ name: "PLAYBACK",
1616
2246
  members: [
1617
2247
  "gui.PLAYBACK_ONCE_FORWARD",
1618
2248
  "gui.PLAYBACK_ONCE_BACKWARD",
@@ -1624,7 +2254,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1624
2254
  },
1625
2255
  {
1626
2256
  home: "gui",
1627
- name: "Property",
2257
+ name: "PROP",
1628
2258
  members: [
1629
2259
  "gui.PROP_POSITION",
1630
2260
  "gui.PROP_ROTATION",
@@ -1643,7 +2273,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1643
2273
  },
1644
2274
  {
1645
2275
  home: "gui",
1646
- name: "SafeAreaMode",
2276
+ name: "SAFE_AREA",
1647
2277
  members: [
1648
2278
  "gui.SAFE_AREA_NONE",
1649
2279
  "gui.SAFE_AREA_LONG",
@@ -1653,22 +2283,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1653
2283
  },
1654
2284
  {
1655
2285
  home: "gui",
1656
- name: "SizeMode",
2286
+ name: "SIZE_MODE",
1657
2287
  members: ["gui.SIZE_MODE_MANUAL", "gui.SIZE_MODE_AUTO"],
1658
2288
  },
1659
- {
1660
- home: "gui",
1661
- name: "XAnchor",
1662
- members: ["gui.ANCHOR_NONE", "gui.ANCHOR_LEFT", "gui.ANCHOR_RIGHT"],
1663
- },
1664
- {
1665
- home: "gui",
1666
- name: "YAnchor",
1667
- members: ["gui.ANCHOR_NONE", "gui.ANCHOR_TOP", "gui.ANCHOR_BOTTOM"],
1668
- },
1669
2289
  {
1670
2290
  home: "profiler",
1671
- name: "Mode",
2291
+ name: "MODE",
1672
2292
  members: [
1673
2293
  "profiler.MODE_RUN",
1674
2294
  "profiler.MODE_PAUSE",
@@ -1678,7 +2298,7 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1678
2298
  },
1679
2299
  {
1680
2300
  home: "profiler",
1681
- name: "ViewMode",
2301
+ name: "VIEW_MODE",
1682
2302
  members: ["profiler.VIEW_MODE_FULL", "profiler.VIEW_MODE_MINIMIZED"],
1683
2303
  },
1684
2304
  {
@@ -1690,9 +2310,15 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1690
2310
  "graphics.BUFFER_TYPE_STENCIL_BIT",
1691
2311
  ],
1692
2312
  },
2313
+ // The render-target creator raises on any other texture type.
2314
+ {
2315
+ home: "render",
2316
+ name: "RenderTargetTextureType",
2317
+ members: ["graphics.TEXTURE_TYPE_2D", "graphics.TEXTURE_TYPE_CUBE_MAP"],
2318
+ },
1693
2319
  {
1694
2320
  home: "sys",
1695
- name: "NetworkConnectivity",
2321
+ name: "NETWORK",
1696
2322
  members: [
1697
2323
  "sys.NETWORK_DISCONNECTED",
1698
2324
  "sys.NETWORK_CONNECTED_CELLULAR",
@@ -1701,12 +2327,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1701
2327
  },
1702
2328
  {
1703
2329
  home: "window",
1704
- name: "DimModeState",
2330
+ name: "DIMMING",
1705
2331
  members: ["window.DIMMING_UNKNOWN", "window.DIMMING_ON", "window.DIMMING_OFF"],
1706
2332
  },
1707
2333
  {
1708
2334
  home: "window",
1709
- name: "DimModeStateSettable",
2335
+ parent: "DIMMING",
1710
2336
  members: ["window.DIMMING_ON", "window.DIMMING_OFF"],
1711
2337
  },
1712
2338
  ];
@@ -1726,11 +2352,45 @@ export function constantUnionAlias(fqns: readonly string[]): ConstantUnionAlias
1726
2352
  return CONSTANT_UNION_ALIAS_BY_MEMBERS.get(constantUnionAliasKey(fqns));
1727
2353
  }
1728
2354
 
1729
- // The alias declarations a module owns, in name order. A row is declared in its
1730
- // home whether or not that module references it, so a cross-namespace row is
1731
- // reachable from the module whose signatures spell it.
1732
- export function constantUnionAliasDeclarations(namespace: string): string[] {
1733
- return CONSTANT_UNION_ALIASES.filter((row) => row.home === namespace)
2355
+ // What a slot taking exactly the row's members is written as: a named row's
2356
+ // qualified name, or a subset row's parent minus the parent's other members,
2357
+ // listed in the parent's order.
2358
+ export function constantUnionSpelling(row: ConstantUnionAlias): string {
2359
+ if ("name" in row) return `${row.home}.${engineTypeName(row.name)}`;
2360
+ const parent = CONSTANT_UNION_ALIASES.find(
2361
+ (candidate): candidate is ConstantUnionNamedRow =>
2362
+ "name" in candidate && candidate.home === row.home && candidate.name === row.parent,
2363
+ );
2364
+ if (parent === undefined) {
2365
+ throw new Error(`constant union subset of ${row.home}.${row.parent}: no such named row`);
2366
+ }
2367
+ const kept = new Set(row.members);
2368
+ const excluded = parent.members.filter((member) => !kept.has(member));
2369
+ return `Exclude<${row.home}.${engineTypeName(row.parent)}, ${excluded.map((m) => `typeof ${m}`).join(" | ")}>`;
2370
+ }
2371
+
2372
+ // The named rows a module owns; a subset row declares nothing. A row is
2373
+ // declared in its home whether or not that module references it, so a
2374
+ // cross-namespace row is reachable from the module whose signatures spell it.
2375
+ // A row whose name the module's own `ENUM` already declares is left to that
2376
+ // `ENUM`, which carries the reference's member order and nilable arms.
2377
+ function ownedConstantUnionAliases(
2378
+ namespace: string,
2379
+ enumDeclared: ReadonlySet<string>,
2380
+ ): ConstantUnionNamedRow[] {
2381
+ return CONSTANT_UNION_ALIASES.filter(
2382
+ (row): row is ConstantUnionNamedRow =>
2383
+ "name" in row && row.home === namespace && !enumDeclared.has(row.name),
2384
+ );
2385
+ }
2386
+
2387
+ // The alias declarations a module owns, in name order.
2388
+ export function constantUnionAliasDeclarations(
2389
+ namespace: string,
2390
+ enumDeclared: ReadonlySet<string> = new Set(),
2391
+ ): string[] {
2392
+ return ownedConstantUnionAliases(namespace, enumDeclared)
2393
+ .map((row) => ({ name: engineTypeName(row.name), members: row.members }))
1734
2394
  .sort((a, b) => a.name.localeCompare(b.name))
1735
2395
  .map((row) => unionAliasDeclaration(row.name, row.members));
1736
2396
  }
@@ -1746,10 +2406,12 @@ function unionAliasDeclaration(
1746
2406
  }
1747
2407
 
1748
2408
  // A slot of a function the authored-overload lane declares, published under
1749
- // one alias on every target so the version-independent overload file can
1750
- // name the type each target's own reference gives that slot.
2409
+ // one name on every target so the version-independent overload file can name
2410
+ // the type each target's own reference gives that slot. The name is the
2411
+ // PascalCase name of the newest reference's own declaration for the slot; a
2412
+ // target that declares it emits no alias.
1751
2413
  export const OVERLOAD_SLOT_ALIASES: ReadonlyMap<string, string> = new Map([
1752
- ["render.render_target:param:parameters", "RenderTargetParams"],
2414
+ ["render.render_target:param:parameters", engineTypeName("render_target_params")],
1753
2415
  ]);
1754
2416
 
1755
2417
  const DOC_CONSTANT_TOKEN =
@@ -1801,9 +2463,14 @@ function constantSlotTokens(
1801
2463
  return documentedConstantTokens(entry.family, universe);
1802
2464
  }
1803
2465
 
1804
- function slotDocLookup(module: ApiModule): SlotDocLookup {
2466
+ // A borrow reads the ref-doc, so a slot of a function the skip rules withheld
2467
+ // (`gui.animate`, which gui-overloads.d.ts declares) still lends its doc.
2468
+ function slotDocLookup(
2469
+ module: ApiModule,
2470
+ withheldFunctions: readonly ApiFunction[] = [],
2471
+ ): SlotDocLookup {
1805
2472
  const docs = new Map<string, string>();
1806
- for (const fn of module.functions) {
2473
+ for (const fn of [...module.functions, ...withheldFunctions]) {
1807
2474
  for (const p of fn.parameters) {
1808
2475
  const key = tableSlotKey(fn.name, "param", p.name);
1809
2476
  if (!docs.has(key)) docs.set(key, p.doc);
@@ -1823,8 +2490,9 @@ export function resolveConstantSlotTokens(
1823
2490
  module: ApiModule,
1824
2491
  slotKey: string,
1825
2492
  universe: ReadonlySet<string>,
2493
+ withheldFunctions: readonly ApiFunction[] = [],
1826
2494
  ): string[] {
1827
- const lookupDoc = slotDocLookup(module);
2495
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1828
2496
  return constantSlotTokens(lookupDoc(slotKey) ?? "", slotKey, lookupDoc, universe);
1829
2497
  }
1830
2498
 
@@ -1838,21 +2506,23 @@ type ConstantSlotTokens = (
1838
2506
  function constantSlotTokenResolver(
1839
2507
  module: ApiModule,
1840
2508
  universe: ReadonlySet<string>,
2509
+ withheldFunctions: readonly ApiFunction[] = [],
1841
2510
  ): ConstantSlotTokens {
1842
- const lookupDoc = slotDocLookup(module);
2511
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1843
2512
  return (elementName, slotKind, slotName, doc) =>
1844
2513
  constantSlotTokens(doc, tableSlotKey(elementName, slotKind, slotName), lookupDoc, universe);
1845
2514
  }
1846
2515
 
1847
- // FQN-keyed allowlist of the `types.is_*` checks that genuinely narrow their
1848
- // argument, mapped to the `DEFOLD_TYPE_MAP` token whose interface they prove.
1849
- // Emitting these as user-defined type guards (`var_ is Vector3`) is the only way
1850
- // to narrow an engine userdata value, which `typeof` cannot. An allowlist, not a
1851
- // blanket `is_*` match: `b2d.body.is_active`, `gui.is_enabled`,
1852
- // `sound.is_music_playing`, `go.exists`, `sys.exists` are runtime-state /
1853
- // existence checks on already-typed arguments and must stay `boolean`. An
1854
- // unlisted future `types.is_*` also stays a visible `boolean` until curated here,
1855
- // matching the ARBITRARY_TABLE_SLOTS deliberate-opt-in precedent.
2516
+ // FQN-keyed allowlist of the checks that genuinely narrow their argument, mapped
2517
+ // to the `DEFOLD_TYPE_MAP` token whose type they prove: the `types.is_*` checks,
2518
+ // and `bullet3d.collision_object.is_rigid_body`, whose binding is a
2519
+ // `btRigidBody::upcast` test. Emitting these as user-defined type guards
2520
+ // (`var_ is Vector3`) is the only way to narrow an engine userdata value, which
2521
+ // `typeof` cannot. An allowlist, not a blanket `is_*` match: `b2d.body.is_active`,
2522
+ // `gui.is_enabled`, `sound.is_music_playing`, `go.exists`, `sys.exists` are
2523
+ // runtime-state / existence checks on already-typed arguments and must stay
2524
+ // `boolean`. An unlisted future `types.is_*` also stays a visible `boolean` until
2525
+ // curated here, matching the ARBITRARY_TABLE_SLOTS deliberate-opt-in precedent.
1856
2526
  export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1857
2527
  ["types.is_hash", "hash"],
1858
2528
  ["types.is_matrix4", "matrix4"],
@@ -1861,13 +2531,47 @@ export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1861
2531
  ["types.is_vector", "vector"],
1862
2532
  ["types.is_vector3", "vector3"],
1863
2533
  ["types.is_vector4", "vector4"],
2534
+ ["bullet3d.collision_object.is_rigid_body", "btRigidBody"],
2535
+ ]);
2536
+
2537
+ // The merged ref-doc gives each Box2D backend's form of these queries one
2538
+ // parameter list, so the first (v2) arm always wins and a v3 hit is unreachable.
2539
+ // A type parameter tells them apart: v2/script_box2d_world_v2.cpp:PushCastHit
2540
+ // sets `fixture` and `shape` to one PushFixtureInfo table, while
2541
+ // v3/script_box2d_world_v3.cpp:PushCastHit sets `shape` alone from
2542
+ // PushShapeInfoForShape. The v2 arm is declared first, so a call with no type
2543
+ // argument keeps the v2 hits; its `= "v2"` default only states that. Keyed by
2544
+ // `box2dBackendArmKey`.
2545
+ export const BOX2D_BACKEND_ARMS: ReadonlyMap<string, Box2dBackend> = new Map<string, Box2dBackend>([
2546
+ ["b2d.world.cast_ray:b2d.fixture_cast_hit[]", "v2"],
2547
+ ["b2d.world.cast_ray:b2d.shape_cast_hit[]", "v3"],
2548
+ ["b2d.world.cast_ray_closest:b2d.fixture_cast_hit", "v2"],
2549
+ ["b2d.world.cast_ray_closest:b2d.shape_cast_hit", "v3"],
2550
+ ["b2d.world.cast_shape:b2d.fixture_cast_hit[]", "v2"],
2551
+ ["b2d.world.cast_shape:b2d.shape_cast_hit[]", "v3"],
2552
+ ["b2d.world.overlap_aabb:b2d.fixture_info[]", "v2"],
2553
+ ["b2d.world.overlap_aabb:b2d.shape_info[]", "v3"],
2554
+ ["b2d.world.overlap_shape:b2d.fixture_info[]", "v2"],
2555
+ ["b2d.world.overlap_shape:b2d.shape_info[]", "v3"],
1864
2556
  ]);
1865
2557
 
2558
+ // `<fqn>:<first non-nil type of the first return>`: the two arms of each
2559
+ // `BOX2D_BACKEND_ARMS` query share every parameter and differ only there.
2560
+ export function box2dBackendArmKey(fn: ApiFunction): string {
2561
+ const first = fn.returnValues[0]?.types.find((type) => type !== "nil") ?? "";
2562
+ return `${fn.name}:${first}`;
2563
+ }
2564
+
2565
+ function box2dBackendTypeParameters(fn: ApiFunction): string {
2566
+ const backend = BOX2D_BACKEND_ARMS.get(box2dBackendArmKey(fn));
2567
+ if (backend === undefined) return "";
2568
+ return backend === "v2" ? '<Backend extends "v2" = "v2">' : '<Backend extends "v3">';
2569
+ }
2570
+
1866
2571
  // Element names whose `table` slot is a prose-only `array/list/table of <T>` shape
1867
2572
  // the field-list parser cannot read, but whose element type a human curated from
1868
2573
  // the doc. The value is a single element token (`T[]`) or a token list when the
1869
- // element is itself a union — `go.delete`'s "table of id's" is the id union
1870
- // `string | hash | url`, emitted `(string | Hash | Url)[]`. Emitted as a plain
2574
+ // element is itself a union, emitted `(A | B)[]`. Emitted as a plain
1871
2575
  // array, never `LuaMap`: an array element carries no illegal-index-key problem so
1872
2576
  // no TSTL wiring is needed. The audit consults this map too, so the gate and the
1873
2577
  // emitted surface stay coupled; a slot not curated here still surfaces under
@@ -1882,7 +2586,6 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
1882
2586
  ["vmath.vector", "number"],
1883
2587
  ["sound.get_groups", "hash"],
1884
2588
  ["iap.list", "string"],
1885
- ["go.delete", ["string", "hash", "url"]],
1886
2589
  // push.register's `notifications` is a prose-only array of push.NOTIFICATION_*
1887
2590
  // bitmask constants (the ref example sums NOTIFICATION_BADGE/SOUND/ALERT). The
1888
2591
  // vendored push_doc.json fixture declares no NOTIFICATION_* constant elements, so
@@ -1905,7 +2608,11 @@ export type NestedMapping = { key: string; value: string };
1905
2608
  // for `collectionfactory.create`'s properties.
1906
2609
  export type TableSlotCuration = (
1907
2610
  | { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
1908
- | { kind: "array"; element: string | readonly string[] }
2611
+ // `pairs` also admits any table whose values are the element, for a binding
2612
+ // that walks the table with lua_next and reads only its values. The extra arm
2613
+ // is `LuaPairsIterable`, which is covariant in its value; `LuaTable` is
2614
+ // invariant through `set`, so it would reject a `LuaTable<Hash, Hash>`.
2615
+ | { kind: "array"; element: string | readonly string[]; pairs?: true }
1909
2616
  | { kind: "object"; fields: readonly TableField[] }
1910
2617
  | { kind: "array-object"; fields: readonly TableField[] }
1911
2618
  // A `<key> -> <args table>` slot: the doc's `<dl>` describes the *value* shape,
@@ -1918,7 +2625,34 @@ export type TableSlotCuration = (
1918
2625
  | { kind: "verbatim"; ts: string }
1919
2626
  ) & { readonly retypedUpstream?: readonly string[] };
1920
2627
 
1921
- export const SOCKET_HANDLE_TOKENS = ["client", "master", "unconnected"] as const;
2628
+ // The interface a module's component properties are declared on; its PROPERTY
2629
+ // entries carry its members, so it signs no TYPEDEF of its own.
2630
+ export const PROPERTIES_INTERFACE = engineTypeName("properties");
2631
+
2632
+ // The ref-doc tokens of the handles `socket.select` takes and returns, in their
2633
+ // 1.13.2 TYPEDEF spelling, which the type map resolves to each handle's interface.
2634
+ export const SOCKET_HANDLE_TOKENS = (["client", "master", "unconnected"] as const).map(
2635
+ (receiver) => `socket_${receiver}`,
2636
+ );
2637
+
2638
+ // The interface a module's `<receiver>:<method>` colon methods are emitted on:
2639
+ // the socket handles take their `Socket<Receiver>` name, any other receiver
2640
+ // keeps its own.
2641
+ export function handleInterfaceName(namespace: string, receiver: string): string {
2642
+ return namespace === "socket" && Object.hasOwn(SOCKET_HANDLE_NAMES, receiver)
2643
+ ? SOCKET_HANDLE_NAMES[receiver as SocketHandleReceiver]
2644
+ : receiver;
2645
+ }
2646
+
2647
+ // The typedef names a module's handle interfaces stand for, which emit as the
2648
+ // interface rather than an alias: each receiver (1.13.1 `client`) and each
2649
+ // interface name, which the 1.13.2 `socket_client` takes once renamed
2650
+ // (`SocketClient`).
2651
+ function handleTypedefNames(namespace: string, receivers: Iterable<string>): Set<string> {
2652
+ return new Set(
2653
+ [...receivers].flatMap((receiver) => [receiver, handleInterfaceName(namespace, receiver)]),
2654
+ );
2655
+ }
1922
2656
 
1923
2657
  // Shared Box2D (b2d.*) table shapes, curated from the ref-doc prose (the docs
1924
2658
  // name fields in running text, not a machine-readable `<dl>`). Reused across the
@@ -2082,25 +2816,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2082
2816
  string,
2083
2817
  TableSlotCuration
2084
2818
  >([
2085
- // gui_script.cpp:LuaGet and LuaSet read the `key` and `keys` options. On a
2086
- // prose `table` slot TABLE_SLOT_FIELD_ADDITIONS supplies them; 1.13.2 types the
2087
- // slots as an inline record and a struct that omit them.
2088
- ...(
2089
- [
2090
- ["gui.get:param:options", "{ index?:integer }"],
2091
- ["gui.set:param:options", "gui.set_options"],
2092
- ] as const
2093
- ).map(
2094
- ([key, retyped]) =>
2095
- [
2096
- key,
2097
- {
2098
- kind: "verbatim",
2099
- ts: "{ index?: number; key?: string | Hash; keys?: (Hash | string)[] }",
2100
- retypedUpstream: [retyped],
2101
- },
2102
- ] as const,
2103
- ),
2104
2819
  // buffer.create's `declaration` is "a table where each entry (table) describes
2105
2820
  // a stream", and upstream's example passes a list of stream records whose
2106
2821
  // `type` is a buffer.VALUE_TYPE_* constant.
@@ -2128,6 +2843,19 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2128
2843
  },
2129
2844
  ],
2130
2845
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
2846
+ // go.delete's "table of id's": gameobject_script.cpp:DeleteGOTable walks any
2847
+ // table with lua_next and resolves each value with ResolveURL, so the id table
2848
+ // collectionfactory.create returns deletes as it is. 1.13.2 retypes the slot
2849
+ // to an id array, which still rejects that table.
2850
+ [
2851
+ "go.delete:param:id",
2852
+ {
2853
+ kind: "array",
2854
+ element: ["string", "hash", "url"],
2855
+ pairs: true,
2856
+ retypedUpstream: ["(string|hash|url)[]"],
2857
+ },
2858
+ ],
2131
2859
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
2132
2860
  // `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
2133
2861
  // hash`). parseTableFields flattens the inner `<dl>` to the top level, which
@@ -2375,70 +3103,14 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2375
3103
  // depth/stencil. Both sides are union tokens the mapping string branch splits,
2376
3104
  // so the key emits the three branded constants and the value `number | Vector4`.
2377
3105
  // The key set is a CONSTANT_UNION_ALIASES row, so the signature carries the
2378
- // `render.ClearBufferKey` alias rather than the three branded arms.
2379
- [
2380
- "render.clear:param:buffers",
2381
- {
2382
- retypedUpstream: ["table<graphics.BUFFER_TYPE, number|vector4>"],
2383
- kind: "mapping",
2384
- key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
2385
- value: "number | vector4",
2386
- },
2387
- ],
2388
- // go.on_input and gui.on_input share the same well-known InputAction shape
2389
- // (the on_input function description lists the well-known fields: value,
2390
- // pressed, released, repeated, x/y/screen_x/screen_y/dx/dy/screen_dx/screen_dy,
2391
- // plus gamepad/touch/text multi-touch fields). The doc markup carries no
2392
- // field list of its own, so the shape is curated verbatim from the prose.
2393
- // Param-side optional fields: each input kind (mouse/key/text/gamepad) sets a
2394
- // different subset, so the call site always sees an `?` on every field.
2395
- // `touch` stays `table` (the nested multi-touch table has no machine-readable
2396
- // field list; mirrors the render.set_render_target.transient recursive case).
2397
- // The element is the bare `on_input` script hook (no namespace prefix), the
2398
- // same form the `on_message` arbitrary-table entry uses. The doc carries a
2399
- // well-formed touch input table, so `touch` is curated as a nested
2400
- // array-of-touch-records (the field has its own `<table>` listing id,
2401
- // pressed, released, tap_count, x, y, dx, dy, acc_x/y/z).
2402
- [
2403
- "on_input:param:action",
2404
- {
2405
- kind: "object",
2406
- fields: [
2407
- { name: "value", types: ["number"], optional: true },
2408
- { name: "pressed", types: ["boolean"], optional: true },
2409
- { name: "released", types: ["boolean"], optional: true },
2410
- { name: "repeated", types: ["boolean"], optional: true },
2411
- { name: "x", types: ["number"], optional: true },
2412
- { name: "y", types: ["number"], optional: true },
2413
- { name: "screen_x", types: ["number"], optional: true },
2414
- { name: "screen_y", types: ["number"], optional: true },
2415
- { name: "dx", types: ["number"], optional: true },
2416
- { name: "dy", types: ["number"], optional: true },
2417
- { name: "screen_dx", types: ["number"], optional: true },
2418
- { name: "screen_dy", types: ["number"], optional: true },
2419
- { name: "gamepad", types: ["number"], optional: true },
2420
- { name: "gamepad_axis", types: ["vector3"], optional: true },
2421
- {
2422
- name: "touch",
2423
- types: ["table"],
2424
- optional: true,
2425
- isList: true,
2426
- fields: [
2427
- { name: "id", types: ["number"] },
2428
- { name: "pressed", types: ["boolean"] },
2429
- { name: "released", types: ["boolean"] },
2430
- { name: "tap_count", types: ["number"] },
2431
- { name: "x", types: ["number"] },
2432
- { name: "y", types: ["number"] },
2433
- { name: "dx", types: ["number"] },
2434
- { name: "dy", types: ["number"] },
2435
- { name: "acc_x", types: ["number"] },
2436
- { name: "acc_y", types: ["number"] },
2437
- { name: "acc_z", types: ["number"] },
2438
- ],
2439
- },
2440
- { name: "text", types: ["string"], optional: true },
2441
- ],
3106
+ // `render.ClearBufferKey` alias rather than the three branded arms.
3107
+ [
3108
+ "render.clear:param:buffers",
3109
+ {
3110
+ retypedUpstream: ["table<graphics.BUFFER_TYPE, number|vector4>"],
3111
+ kind: "mapping",
3112
+ key: "graphics.BUFFER_TYPE_COLOR0_BIT | graphics.BUFFER_TYPE_DEPTH_BIT | graphics.BUFFER_TYPE_STENCIL_BIT",
3113
+ value: "number | vector4",
2442
3114
  },
2443
3115
  ],
2444
3116
  // physics.create_joint and physics.set_joint_properties both declare
@@ -2932,7 +3604,9 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2932
3604
  // no `status`; typing it as its own shape needs a per-target authored overload,
2933
3605
  // so `status` stays required and the slot's `note` says how to tell the two
2934
3606
  // apart. Its typed form is the same signature: the typed reference names
2935
- // `http.response`, which the runtime surface withholds.
3607
+ // `http.response`, which the runtime surface withholds. Both forms add the
3608
+ // `sender` no reference lists: comp_script.cpp delivers the response as a
3609
+ // message, with `PushURL(L, message->m_Sender)` then `PCall(L, 4, 0)`.
2936
3610
  // `model.play_anim`'s completion callback receives self, the message id and the
2937
3611
  // message only: script_model.cpp calls it with `PCall(L, 3, 0)`, so both forms
2938
3612
  // drop the `sender` every reference lists and type the message as the typed
@@ -2973,13 +3647,13 @@ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, CallbackSignature
2973
3647
  "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: Record<string | number, unknown>) => void",
2974
3648
  typed:
2975
3649
  // biome-ignore lint/suspicious/noTemplateCurlyInString: emitted verbatim as a TS template literal type
2976
- "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: window.event_data) => void",
3650
+ "(self: unknown, event: (typeof window)[Extract<keyof typeof window, `WINDOW_EVENT_${string}`>], data: window.EventData) => void",
2977
3651
  },
2978
3652
  ],
2979
3653
  [
2980
3654
  "http.request:param:callback",
2981
3655
  onBothForms(
2982
- "(self: unknown, id: Hash, response: { status: number; url: string; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_sent?: number; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }) => void",
3656
+ "(self: unknown, id: Hash, response: { status: number; url: string; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_sent?: number; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }, sender: Url) => void",
2983
3657
  "With `report_progress` set, the callback also receives progress events carrying `bytes_sent`, `bytes_received`, `bytes_total` and `url` but no `status`; `response.bytes_received !== undefined` marks a progress event, since a final response never carries it.",
2984
3658
  ),
2985
3659
  ],
@@ -3041,9 +3715,11 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
3041
3715
  // `<element>:<kind>:<slot>:<field>`, mirroring NESTED_FIELD_CURATIONS; a field
3042
3716
  // one level down is `<field>.<member>`, or `<field>[].<member>` inside a list.
3043
3717
  //
3044
- // The atlas animation `playback` field is documented as `constant` with the
3045
- // prose "the default value is go.PLAYBACK_ONCE_FORWARD", which names the enum
3046
- // the bare token cannot.
3718
+ // Unpinned records below rest on ref-doc tokens or prose. The atlas animation
3719
+ // `playback` field is documented as `constant` with the prose "the default value
3720
+ // is go.PLAYBACK_ONCE_FORWARD", which names the enum the bare token cannot; its
3721
+ // records also carry engine binding pins and apply only when the matching
3722
+ // `EmitOptions.bindingEvidence` key holds for the target.
3047
3723
  //
3048
3724
  // `gui.set`'s `options.key` is documented as `hash`, while upstream's own
3049
3725
  // example sets a named font with `{ key = "my_font_name" }`.
@@ -3056,21 +3732,47 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
3056
3732
  // script_resource.cpp:CreateSoundData reads it with CheckBufferOrString.
3057
3733
  const CONSTANT_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | (Vector4 | Matrix4)[]";
3058
3734
  const ATTRIBUTE_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | number[]";
3059
- export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
3060
- ["compute.set_constants:param:constants:value", CONSTANT_VALUE_TS],
3061
- ["material.set_constants:param:constants:value", CONSTANT_VALUE_TS],
3062
- ["material.set_vertex_attributes:param:attributes:value", ATTRIBUTE_VALUE_TS],
3063
- ["material.get_vertex_attributes:return:table:value", ATTRIBUTE_VALUE_TS],
3064
- ["resource.set_atlas:param:table:animations[].playback", "go.Playback"],
3065
- ["resource.create_atlas:param:table:animations[].playback", "go.Playback"],
3066
- ["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
3067
- ["gui.set:param:options:key", "string | Hash"],
3068
- ["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
3069
- ["resource.create_sound_data:param:options:data", 'string | Opaque<"buffer">'],
3070
- // The same two slots where 1.13.2 types the option bag as a STRUCT, whose
3071
- // members still carry the narrower upstream types.
3072
- ["gui.set_options:member:key", "string | Hash"],
3073
- ["resource.sound_data_options:member:data", 'string | Opaque<"buffer">'],
3735
+ const ATLAS_BINDING_FILE = "gamesys/src/gamesys/scripts/script_resource.cpp";
3736
+ const ATLAS_INPUT_PLAYBACK_BINDING: BindingPin = {
3737
+ file: ATLAS_BINDING_FILE,
3738
+ cFunction: "MakeTextureSetFromLua",
3739
+ contains: "GameObjectPlaybackToDDFPlayback((dmGameObject::Playback) lua_tointeger(L,-1))",
3740
+ };
3741
+ const ATLAS_OUTPUT_PLAYBACK_BINDING: BindingPin = {
3742
+ file: ATLAS_BINDING_FILE,
3743
+ cFunction: "GetAtlas",
3744
+ contains: "(lua_Integer) DDFPlaybackToGameObjectPlayback(anim.m_Playback)",
3745
+ };
3746
+
3747
+ export interface TableFieldTypeOverride {
3748
+ readonly ts: string;
3749
+ readonly binding?: BindingPin;
3750
+ }
3751
+
3752
+ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, TableFieldTypeOverride> = new Map([
3753
+ ["compute.set_constants:param:constants:value", { ts: CONSTANT_VALUE_TS }],
3754
+ ["material.set_constants:param:constants:value", { ts: CONSTANT_VALUE_TS }],
3755
+ ["material.set_vertex_attributes:param:attributes:value", { ts: ATTRIBUTE_VALUE_TS }],
3756
+ ["material.get_vertex_attributes:return:table:value", { ts: ATTRIBUTE_VALUE_TS }],
3757
+ [
3758
+ "resource.set_atlas:param:table:animations[].playback",
3759
+ { ts: "go.Playback", binding: ATLAS_INPUT_PLAYBACK_BINDING },
3760
+ ],
3761
+ [
3762
+ "resource.create_atlas:param:table:animations[].playback",
3763
+ { ts: "go.Playback", binding: ATLAS_INPUT_PLAYBACK_BINDING },
3764
+ ],
3765
+ [
3766
+ "resource.get_atlas:return:data:animations[].playback",
3767
+ { ts: "go.Playback", binding: ATLAS_OUTPUT_PLAYBACK_BINDING },
3768
+ ],
3769
+ ["render.set_render_target:param:options:transient", { ts: "graphics.BufferType[]" }],
3770
+ ["resource.create_sound_data:param:options:data", { ts: 'string | Opaque<"buffer">' }],
3771
+ // The option bags 1.13.2 types as a STRUCT, whose members still carry the
3772
+ // narrower upstream types: `gui.set`'s (its slot is declared in
3773
+ // gui-overloads.d.ts) and `resource.create_sound_data`'s.
3774
+ ["gui.set_options:member:key", { ts: "string | Hash" }],
3775
+ ["resource.sound_data_options:member:data", { ts: 'string | Opaque<"buffer">' }],
3074
3776
  // Bitmasks typed as their flag enum: the create_texture example passes
3075
3777
  // TEXTURE_USAGE_FLAG_STORAGE + TEXTURE_USAGE_FLAG_SAMPLE, a sum no single member
3076
3778
  // names, and a reported value combines them the same way.
@@ -3081,8 +3783,15 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
3081
3783
  "resource.render_target_attachment_info",
3082
3784
  "material.texture_info",
3083
3785
  ] as const
3084
- ).map((struct) => [`${struct}:member:flags`, "graphics.TEXTURE_USAGE_FLAG | number"] as const),
3085
- ["render.render_target_buffer_params:member:flags", "render.RENDER_TARGET_FLAG | number"],
3786
+ ).map(
3787
+ (struct) => [`${struct}:member:flags`, { ts: "graphics.TextureUsageFlag | number" }] as const,
3788
+ ),
3789
+ ["render.render_target_buffer_params:member:flags", { ts: "render.RenderTargetFlag | number" }],
3790
+ // gamesys_script.cpp:PushSampler, behind both get_samplers, pushes the raw
3791
+ // texture type; NormalizeBindableTextureType never runs there. A separate
3792
+ // sampler reports TEXTURE_TYPE_SAMPLER (6) and a separate texture binding one
3793
+ // of the four TEXTURE_TYPE_TEXTURE_* (7-10), none of which has a Lua constant.
3794
+ ["material.sampler_info:member:type", { ts: "graphics.TextureType | 6 | 7 | 8 | 9 | 10" }],
3086
3795
  ]);
3087
3796
 
3088
3797
  // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
@@ -3102,13 +3811,6 @@ export interface TableFieldAddition {
3102
3811
  readonly evidence: readonly string[];
3103
3812
  }
3104
3813
 
3105
- const PROPERTY_OPTION_KEYS: TableField = {
3106
- name: "keys",
3107
- types: ["table"],
3108
- optional: true,
3109
- tsType: "(Hash | string)[]",
3110
- };
3111
-
3112
3814
  export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map<
3113
3815
  string,
3114
3816
  TableFieldAddition
@@ -3123,24 +3825,6 @@ export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition>
3123
3825
  ],
3124
3826
  },
3125
3827
  ],
3126
- [
3127
- "gui.get:param:options",
3128
- {
3129
- fields: [{ name: "key", types: ["string", "hash"], optional: true }, PROPERTY_OPTION_KEYS],
3130
- evidence: [
3131
- "gui_script.cpp:LuaGet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
3132
- ],
3133
- },
3134
- ],
3135
- [
3136
- "gui.set:param:options",
3137
- {
3138
- fields: [PROPERTY_OPTION_KEYS],
3139
- evidence: [
3140
- "gui_script.cpp:LuaSet reads its options through dmGameObject::LuaToPropertyOptions, which reads index, key and keys",
3141
- ],
3142
- },
3143
- ],
3144
3828
  ]);
3145
3829
 
3146
3830
  // A return-slot field upstream documents as platform-gated or nil-valued while
@@ -3538,12 +4222,16 @@ export function applyFieldTypeOverrides(
3538
4222
  slotKind: "param" | "return" | undefined,
3539
4223
  slotName: string | undefined,
3540
4224
  fields: readonly TableField[],
4225
+ bindingEvidence: ReadonlySet<string>,
3541
4226
  ): TableField[] {
3542
4227
  if (slotKind === undefined || slotName === undefined) return [...fields];
3543
4228
  const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
3544
4229
  const typed = (field: TableField, path: string): TableField => {
3545
- const override = TABLE_FIELD_TYPE_OVERRIDES.get(`${prefix}${path}`);
3546
- return override === undefined ? field : { ...field, tsType: override };
4230
+ const key = `${prefix}${path}`;
4231
+ const override = TABLE_FIELD_TYPE_OVERRIDES.get(key);
4232
+ return override === undefined || (override.binding !== undefined && !bindingEvidence.has(key))
4233
+ ? field
4234
+ : { ...field, tsType: override.ts };
3547
4235
  };
3548
4236
  return fields.map((field) => {
3549
4237
  const own = typed(field, field.name);
@@ -3890,22 +4578,84 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
3890
4578
  return grouped;
3891
4579
  }
3892
4580
 
4581
+ // A TYPEDEF whose aliased type the engine binding contradicts, replaced verbatim.
4582
+ // Member overrides reach only a STRUCT, and these alias inline LuaLS tables.
4583
+ // Each applies only while the typedef still aliases exactly its `upstream` tokens,
4584
+ // in order, so a changed upstream typedef is emitted as written rather than
4585
+ // shadowed; `typedef-correction-provenance.test.ts` pins every entry against the
4586
+ // retained ref-docs. Keyed by the typedef's ref-doc name.
4587
+ export interface TypedefAliasCorrection {
4588
+ readonly upstream: readonly string[];
4589
+ readonly ts: string;
4590
+ readonly reason: string;
4591
+ }
4592
+
4593
+ // One arm per shape a script can build, in upstream's order, each keyed by its
4594
+ // own shape-type constant.
4595
+ const BULLET3D_SHAPE_ARMS = [
4596
+ ["SPHERE", "diameter: number"],
4597
+ ["BOX", "dimensions: Vector3"],
4598
+ ["CAPSULE", "diameter: number; height: number"],
4599
+ ["HULL", "vertices: Vector3[]"],
4600
+ ] as const;
4601
+
4602
+ export const TYPEDEF_ALIAS_CORRECTIONS: ReadonlyMap<string, TypedefAliasCorrection> = new Map([
4603
+ [
4604
+ "render.render_target_params",
4605
+ {
4606
+ upstream: [
4607
+ "{ type?:graphics.TEXTURE_TYPE, sample_count?:integer, [graphics.BUFFER_TYPE]:render.render_target_buffer_params }",
4608
+ ],
4609
+ ts: "{ type?: render.RenderTargetTextureType; sample_count?: number } & { [key: number]: render.RenderTargetBufferParams }",
4610
+ reason:
4611
+ 'render_script.cpp:RenderScript_RenderTarget raises "Invalid render target texture type" on any `type` but graphics.TEXTURE_TYPE_2D and graphics.TEXTURE_TYPE_CUBE_MAP',
4612
+ },
4613
+ ],
4614
+ [
4615
+ "bullet3d.shape.definition",
4616
+ {
4617
+ upstream: [
4618
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4619
+ "{ type:bullet3d.shape.SHAPE_TYPE, dimensions:vector3, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4620
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, height:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4621
+ "{ type:bullet3d.shape.SHAPE_TYPE, vertices:vector3[], position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4622
+ ],
4623
+ ts: BULLET3D_SHAPE_ARMS.map(
4624
+ ([kind, fields]) =>
4625
+ `{ type: typeof bullet3d.shape.SHAPE_TYPE_${kind}; ${fields}; position?: Vector3; rotation?: Quaternion; target_rotation?: Quaternion }`,
4626
+ ).join(" | "),
4627
+ reason:
4628
+ 'script_bullet3d_shape.cpp:CheckBullet3DShapeDef reads only the fields of the table\'s own `type` and raises "Unsupported shape type" on any other, a mesh included',
4629
+ },
4630
+ ],
4631
+ ]);
4632
+
3893
4633
  // A handle the type map widens (`constant_buffer` takes named constants) keeps
3894
4634
  // that shape, so annotating with the alias loses nothing the handle-returning
3895
4635
  // function yields.
3896
4636
  //
3897
4637
  // An engine typedef that names its aliased type: a `userdata`/`number` handle the
3898
- // type map resolves (`vector`, `socket_client`) aliases that mapping, an
4638
+ // type map resolves (`timer_handle`, `buffer_data`) aliases that mapping, an
3899
4639
  // unresolved one keeps the brand, and any other aliased type (a union, literals, a
3900
- // record) aliases the type itself.
4640
+ // record) aliases the type itself. A handle the type map resolves to a core
4641
+ // global (`vector` -> `Vector`, `resource_data` -> `Hash`) declares nothing, since
4642
+ // its slots already name the global: `null`.
3901
4643
  function typedefAlias(
3902
4644
  typedef: ApiTypedef,
3903
4645
  local: string,
3904
4646
  rawMapType: (token: string) => string,
3905
4647
  mapType: DefoldTypeMapper,
3906
- ): string {
4648
+ ): string | null {
3907
4649
  const brand = `Opaque<"${typedef.name}">`;
3908
4650
  const aliasOf = typedef.aliasOf ?? [];
4651
+ const correction = TYPEDEF_ALIAS_CORRECTIONS.get(typedef.name);
4652
+ if (
4653
+ correction !== undefined &&
4654
+ aliasOf.length === correction.upstream.length &&
4655
+ aliasOf.every((token, i) => token === correction.upstream[i])
4656
+ ) {
4657
+ return `type ${local} = ${correction.ts};`;
4658
+ }
3909
4659
  if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
3910
4660
  const armType: DefoldTypeMapper = DISCRIMINATED_MESSAGE_UNIONS.has(typedef.name)
3911
4661
  ? (token, position) => discriminatedMessageArm(token) ?? mapType(token, position)
@@ -3914,10 +4664,23 @@ function typedefAlias(
3914
4664
  }
3915
4665
  const mapped = rawMapType(typedef.name);
3916
4666
  if (mapped.startsWith(brand)) return `type ${local} = ${mapped};`;
4667
+ if (aliasOf.length > 0 && CORE_GLOBAL_TYPES.has(mapped)) return null;
3917
4668
  const resolved = aliasOf.length > 0 && mapped !== "unknown" && mapped !== local;
3918
4669
  return `type ${local} = ${resolved ? mapped : brand};`;
3919
4670
  }
3920
4671
 
4672
+ // The global types `core-types.ts` declares that the engine vocabulary maps a
4673
+ // handle to.
4674
+ const CORE_GLOBAL_TYPES: ReadonlySet<string> = new Set([
4675
+ "Hash",
4676
+ "Matrix4",
4677
+ "Quaternion",
4678
+ "Url",
4679
+ "Vector",
4680
+ "Vector3",
4681
+ "Vector4",
4682
+ ]);
4683
+
3921
4684
  function isHandleAlias(aliasOf: readonly string[]): boolean {
3922
4685
  return aliasOf.length === 1 && (aliasOf[0] === "userdata" || aliasOf[0] === "number");
3923
4686
  }
@@ -3934,8 +4697,9 @@ interface TypeDeclaration {
3934
4697
 
3935
4698
  // A typedef with no aliased type keeps the flat-name rule it always had; one that
3936
4699
  // names its aliased type, and every struct, sits at its name less the module
3937
- // prefix, nested when that local is dotted. Typedefs come before structs, each in
3938
- // document order.
4700
+ // prefix, nested when that local is dotted. Every leaf goes through
4701
+ // `engineTypeName`, and `skip` matches a typedef by its reference local or its
4702
+ // renamed path. Typedefs come before structs, each in document order.
3939
4703
  function collectTypeDeclarations(
3940
4704
  module: ApiModule,
3941
4705
  skip: ReadonlySet<string>,
@@ -3952,12 +4716,15 @@ function collectTypeDeclarations(
3952
4716
  for (const t of module.typedefs) {
3953
4717
  const local =
3954
4718
  t.aliasOf === undefined ? (TS_IDENTIFIER.test(t.name) ? t.name : null) : localOf(t.name);
3955
- if (local === null || skip.has(local)) continue;
3956
- const name = local.slice(local.lastIndexOf(".") + 1);
4719
+ if (local === null) continue;
4720
+ const path = renamedLeaf(local);
4721
+ if (skip.has(local) || skip.has(path)) continue;
4722
+ const name = path.slice(path.lastIndexOf(".") + 1);
3957
4723
  const signature = typedefAlias(t, name, rawMapType, mapType);
4724
+ if (signature === null) continue;
3958
4725
  out.push({
3959
4726
  name,
3960
- path: local,
4727
+ path,
3961
4728
  lines: (indent, decl) => [`${indent}${decl}${signature}`],
3962
4729
  signature,
3963
4730
  });
@@ -4015,11 +4782,12 @@ function correctedListMemberType(
4015
4782
 
4016
4783
  function structDeclaration(
4017
4784
  struct: ApiStruct,
4018
- path: string,
4785
+ local: string,
4019
4786
  mapType: DefoldTypeMapper,
4020
4787
  structs: readonly ApiStruct[],
4021
4788
  bindingEvidence: ReadonlySet<string>,
4022
4789
  ): TypeDeclaration {
4790
+ const path = renamedLeaf(local);
4023
4791
  const name = path.slice(path.lastIndexOf(".") + 1);
4024
4792
  const memberPrefix = memberCorrectionKey(struct.name, "");
4025
4793
  const declared = new Set(struct.members.map((member) => member.name));
@@ -4046,7 +4814,9 @@ function structDeclaration(
4046
4814
  const members = [...struct.members, ...added].map((member) => {
4047
4815
  const key = memberCorrectionKey(struct.name, member.name);
4048
4816
  const list = LIST_FIELD_CORRECTIONS.get(key);
4049
- const retyped = TABLE_FIELD_TYPE_OVERRIDES.get(key);
4817
+ const listedRetype = TABLE_FIELD_TYPE_OVERRIDES.get(key);
4818
+ const retyped =
4819
+ listedRetype?.binding === undefined || bindingEvidence.has(key) ? listedRetype : undefined;
4050
4820
  const optional =
4051
4821
  (member.isOptional || OPTIONAL_MEMBER_CORRECTIONS.has(key)) &&
4052
4822
  !REQUIRED_FIELD_CORRECTIONS.has(key);
@@ -4054,7 +4824,7 @@ function structDeclaration(
4054
4824
  list !== undefined
4055
4825
  ? correctedListMemberType(struct, member, list, structs, mapType)
4056
4826
  : retyped !== undefined
4057
- ? retyped
4827
+ ? retyped.ts
4058
4828
  : member.type === ""
4059
4829
  ? "unknown"
4060
4830
  : mapType(member.type);
@@ -4078,7 +4848,10 @@ function structDeclaration(
4078
4848
  `${indent}}`,
4079
4849
  ];
4080
4850
  },
4081
- signature: `interface ${name} { ${members.map((member) => `${member.line} `).join("")}}`,
4851
+ signature: interfaceSignature(
4852
+ name,
4853
+ members.map((member) => member.line),
4854
+ ),
4082
4855
  };
4083
4856
  }
4084
4857
 
@@ -4137,8 +4910,9 @@ function moduleTypeMappers(
4137
4910
  * dropped, so the home's own copy wins; a type sharing a value's name
4138
4911
  * (`resource.atlas`) is untouched. A function moves the same way (1.13.2 files
4139
4912
  * `bullet3d.collision_object.get_shapes` in the shape doc), since the emitter
4140
- * declares only the functions under a module's own namespace. Modules with
4141
- * nothing moved in or out are returned as-is.
4913
+ * declares only the functions under a module's own namespace. A moved type
4914
+ * records the module that declared it as `declaredIn`. Modules with nothing
4915
+ * moved in or out are returned as-is.
4142
4916
  */
4143
4917
  export function routeTypeDeclarations(modules: readonly ApiModule[]): ApiModule[] {
4144
4918
  const homeOf = (name: string, declaring: string): string => {
@@ -4168,14 +4942,14 @@ export function routeTypeDeclarations(modules: readonly ApiModule[]): ApiModule[
4168
4942
  const structs = (module.structs ?? []).filter((struct) => {
4169
4943
  const home = homeOf(struct.name, module.namespace);
4170
4944
  if (home === module.namespace) return true;
4171
- into(home).structs.push(struct);
4945
+ into(home).structs.push({ ...struct, declaredIn: module.namespace });
4172
4946
  return false;
4173
4947
  });
4174
4948
  const typedefs = module.typedefs.filter((typedef) => {
4175
4949
  if (typedef.aliasOf === undefined) return true;
4176
4950
  const home = homeOf(typedef.name, module.namespace);
4177
4951
  if (home === module.namespace) return true;
4178
- into(home).typedefs.push(typedef);
4952
+ into(home).typedefs.push({ ...typedef, declaredIn: module.namespace });
4179
4953
  return false;
4180
4954
  });
4181
4955
  const functions = module.functions.filter((fn) => {
@@ -4218,27 +4992,32 @@ export function routeTypeDeclarations(modules: readonly ApiModule[]): ApiModule[
4218
4992
  /**
4219
4993
  * The TS path of every `ENUM`, `STRUCT` and aliasing `TYPEDEF` the (routed)
4220
4994
  * modules declare, keyed by its Lua name: the name itself when it sits under its
4221
- * module's namespace, else that name nested under the module
4222
- * (`on_input.action` in `go` is `go.on_input.action`).
4995
+ * module's namespace, else that name nested under the module, with its leaf
4996
+ * through `engineTypeName` (`on_input.action` in `go` is `go.on_input.Action`).
4223
4997
  */
4224
4998
  export function declaredTypePaths(modules: readonly ApiModule[]): Map<string, string> {
4225
4999
  const paths = new Map<string, string>();
4226
5000
  for (const module of modules) {
4227
- const names = [
4228
- ...(module.enums ?? []).map((e) => e.name),
4229
- ...(module.structs ?? []).map((struct) => struct.name),
4230
- ...module.typedefs.filter((t) => t.aliasOf !== undefined).map((t) => t.name),
4231
- ];
4232
- for (const name of names) {
4233
- paths.set(
4234
- name,
4235
- name.startsWith(`${module.namespace}.`) ? name : `${module.namespace}.${name}`,
4236
- );
5001
+ const qualified = (name: string): string =>
5002
+ name.startsWith(`${module.namespace}.`) ? name : `${module.namespace}.${name}`;
5003
+ for (const { name } of [
5004
+ ...(module.enums ?? []),
5005
+ ...(module.structs ?? []),
5006
+ ...module.typedefs.filter((t) => t.aliasOf !== undefined),
5007
+ ]) {
5008
+ paths.set(name, renamedLeaf(qualified(name)));
4237
5009
  }
4238
5010
  }
4239
5011
  return paths;
4240
5012
  }
4241
5013
 
5014
+ // A dotted type path with its last segment through `engineTypeName`: the
5015
+ // namespace segments mirror the engine's functions and stay as they are.
5016
+ function renamedLeaf(path: string): string {
5017
+ const dot = path.lastIndexOf(".");
5018
+ return `${path.slice(0, dot + 1)}${engineTypeName(path.slice(dot + 1))}`;
5019
+ }
5020
+
4242
5021
  export function emitDeclarations(documented: ApiModule, options?: EmitOptions): string {
4243
5022
  const module = withUndocumentedConstants(documented, options?.registeredConstants);
4244
5023
  const prefix = `${module.namespace}.`;
@@ -4248,11 +5027,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4248
5027
  const translations = options?.translations ?? {};
4249
5028
  const urlParameters = options?.urlParameters ?? [];
4250
5029
  const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
5030
+ const nilRaisingSlots = options?.nilRaisingSlots;
4251
5031
  const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
4252
5032
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4253
5033
  const constantTokens = constantSlotTokenResolver(
4254
5034
  module,
4255
5035
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
5036
+ options?.withheldFunctions,
4256
5037
  );
4257
5038
 
4258
5039
  const constants = module.constants
@@ -4285,6 +5066,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4285
5066
  const nestedRoot = collectNestedGroups(module, prefix);
4286
5067
  const enumAliases = collectEnumAliases(module, constants, nestedRoot);
4287
5068
 
5069
+ const resolver = buildTableDocResolver(
5070
+ module.functions.map((fn) => ({
5071
+ name: fn.name,
5072
+ slots: [...fn.parameters, ...fn.returnValues],
5073
+ })),
5074
+ );
5075
+
4288
5076
  // Colon methods (`client:send`) are FUNCTION elements named `<receiver>:<method>`
4289
5077
  // and are NOT namespace-prefixed, so they fail the flat-identifier test in
4290
5078
  // prepareFunction (the colon) and are absent from `functions`. A returned handle
@@ -4292,27 +5080,26 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4292
5080
  // interface, the same non-flat recovery the dns nested-namespace pass does one
4293
5081
  // container shape shallower.
4294
5082
  const handleGroups = collectHandleMethodGroups(module);
4295
- const handleReceivers = [...handleGroups.keys()].sort((a, b) => a.localeCompare(b));
4296
-
4297
- const resolver = buildTableDocResolver(
4298
- module.functions.map((fn) => ({
4299
- name: fn.name,
4300
- slots: [...fn.parameters, ...fn.returnValues],
4301
- })),
5083
+ const handleInterfaces = collectHandleInterfaces(
5084
+ module.namespace,
5085
+ handleGroups,
5086
+ mapType,
5087
+ resolver,
5088
+ constantTokens,
5089
+ urlParameters,
5090
+ bindingEvidence,
4302
5091
  );
4303
5092
 
4304
5093
  // A typedef that also has colon methods is emitted as a method-bearing
4305
5094
  // interface below, not an opaque brand alias.
4306
- const typeDeclarations = placeTypeDeclarations(
4307
- collectTypeDeclarations(
4308
- module,
4309
- new Set(handleGroups.keys()),
4310
- rawMapType,
4311
- mapType,
4312
- bindingEvidence,
4313
- ),
4314
- nestedRoot,
5095
+ const collectedTypes = collectTypeDeclarations(
5096
+ module,
5097
+ handleTypedefNames(module.namespace, handleGroups.keys()),
5098
+ rawMapType,
5099
+ mapType,
5100
+ bindingEvidence,
4315
5101
  );
5102
+ const typeDeclarations = placeTypeDeclarations(collectedTypes, nestedRoot);
4316
5103
 
4317
5104
  // A re-export alias (`export { _x as x }`) switches the ambient namespace out
4318
5105
  // of its implicit-export mode, so once any alias is present every sibling
@@ -4329,18 +5116,22 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4329
5116
  lines.push(`declare namespace ${module.namespace} {`);
4330
5117
 
4331
5118
  for (const declaration of typeDeclarations) lines.push(...declaration.lines(INDENT, decl));
4332
- for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
5119
+ for (const aliasDecl of constantUnionAliasDeclarations(
5120
+ module.namespace,
5121
+ new Set(enumAliases.all.map((alias) => alias.path)),
5122
+ )) {
4333
5123
  lines.push(`${INDENT}${decl}${aliasDecl}`);
4334
5124
  }
4335
- for (const aliasDecl of overloadSlotAliasDeclarations(
5125
+ for (const { declaration } of overloadSlotAliasDeclarations(
4336
5126
  [...module.functions, ...(options?.withheldFunctions ?? [])],
5127
+ collectedTypes,
4337
5128
  prefix,
4338
5129
  mapType,
4339
5130
  constantTokens,
4340
5131
  urlParameters,
4341
5132
  bindingEvidence,
4342
5133
  )) {
4343
- lines.push(`${INDENT}${decl}${aliasDecl}`);
5134
+ lines.push(`${INDENT}${decl}${declaration}`);
4344
5135
  }
4345
5136
  for (const alias of enumAliases.topLevel) {
4346
5137
  lines.push(
@@ -4348,16 +5139,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4348
5139
  );
4349
5140
  }
4350
5141
  const handleIndent = `${INDENT}${INDENT}`;
4351
- for (const receiver of handleReceivers) {
4352
- const group = handleGroups.get(receiver) ?? [];
4353
- lines.push(`${INDENT}${decl}interface ${receiver} {`);
4354
- for (const fn of group) {
5142
+ for (const { name, members } of handleInterfaces) {
5143
+ lines.push(`${INDENT}${decl}interface ${name} {`);
5144
+ for (const { fn, text } of members) {
4355
5145
  for (const docLine of functionDocLines(fn.original, translations, baseNotes, handleIndent)) {
4356
5146
  lines.push(docLine);
4357
5147
  }
4358
- lines.push(
4359
- `${handleIndent}${emitMethod(fn, mapType, resolver, constantTokens, urlParameters, bindingEvidence)}`,
4360
- );
5148
+ lines.push(`${handleIndent}${text}`);
4361
5149
  }
4362
5150
  lines.push(`${INDENT}}`);
4363
5151
  }
@@ -4385,18 +5173,20 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4385
5173
  for (const fn of functions) {
4386
5174
  const reserved = TS_RESERVED_NAMES.has(fn.name);
4387
5175
  const emitName = aliasName(fn.name, aliases);
4388
- for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4389
- lines.push(docLine);
4390
- const line = emitFunction(
4391
- fn,
4392
- emitName,
4393
- mapType,
4394
- resolver,
4395
- constantTokens,
4396
- urlParameters,
4397
- bindingEvidence,
4398
- );
4399
- lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
5176
+ for (const arm of callArms(fn, nilRaisingSlots, module.structs ?? [])) {
5177
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes))
5178
+ lines.push(docLine);
5179
+ const line = emitFunction(
5180
+ arm,
5181
+ emitName,
5182
+ mapType,
5183
+ resolver,
5184
+ constantTokens,
5185
+ urlParameters,
5186
+ bindingEvidence,
5187
+ );
5188
+ lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
5189
+ }
4400
5190
  const alternative = firstSlotAlternative(fn, module.namespace);
4401
5191
  if (alternative !== null) {
4402
5192
  // The overload documents its own parameters, so its hover names the base
@@ -4501,9 +5291,9 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4501
5291
 
4502
5292
  if (module.properties.length > 0) {
4503
5293
  const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
4504
- lines.push(`${INDENT}${decl}interface properties {`);
5294
+ lines.push(`${INDENT}${decl}interface ${PROPERTIES_INTERFACE} {`);
4505
5295
  for (const p of members) {
4506
- for (const member of emitPropertyMembers(p, mapType, module.namespace)) {
5296
+ for (const member of emitPropertyMembers(p, mapType, module.namespace, bindingEvidence)) {
4507
5297
  for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
4508
5298
  lines.push(docLine);
4509
5299
  }
@@ -4518,17 +5308,25 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4518
5308
  }
4519
5309
 
4520
5310
  // One `type <alias> = <slot type>;` per `OVERLOAD_SLOT_ALIASES` slot these
4521
- // functions carry, in alias order. The type is the one `emitFunction` reports
4522
- // for the slot, so it reads as the function's own signature would. The
4523
- // resolver spans these functions alone, leaving the module's own untouched.
5311
+ // functions carry, in alias order, skipping an alias one of the module's
5312
+ // top-level type `declarations` already names. The type is the one
5313
+ // `emitFunction` reports for the slot, so it reads as the function's own
5314
+ // signature would. The resolver spans these functions alone, leaving the
5315
+ // module's own untouched.
4524
5316
  function overloadSlotAliasDeclarations(
4525
5317
  functions: readonly ApiFunction[],
5318
+ declarations: readonly TypeDeclaration[],
4526
5319
  prefix: string,
4527
5320
  mapType: DefoldTypeMapper,
4528
5321
  constantTokens: ConstantSlotTokens,
4529
5322
  urlParameters: UrlParameterTable,
4530
5323
  bindingEvidence: ReadonlySet<string>,
4531
- ): string[] {
5324
+ ): { name: string; declaration: string }[] {
5325
+ const declared = new Set(
5326
+ declarations
5327
+ .filter((declaration) => declaration.path === declaration.name)
5328
+ .map((declaration) => declaration.name),
5329
+ );
4532
5330
  const resolver = buildTableDocResolver(
4533
5331
  functions.map((fn) => ({ name: fn.name, slots: [...fn.parameters, ...fn.returnValues] })),
4534
5332
  );
@@ -4536,7 +5334,7 @@ function overloadSlotAliasDeclarations(
4536
5334
  for (const fn of functions) {
4537
5335
  for (const [position, p] of fn.parameters.entries()) {
4538
5336
  const alias = OVERLOAD_SLOT_ALIASES.get(tableSlotKey(fn.name, "param", p.name));
4539
- if (alias === undefined) continue;
5337
+ if (alias === undefined || declared.has(alias)) continue;
4540
5338
  const prepared = prepareFunction(fn, prefix);
4541
5339
  if (prepared === null) continue;
4542
5340
  const slots: Record<string, SlotType> = {};
@@ -4563,7 +5361,51 @@ function overloadSlotAliasDeclarations(
4563
5361
  }
4564
5362
  return [...types]
4565
5363
  .sort(([a], [b]) => a.localeCompare(b))
4566
- .map(([alias, type]) => `type ${alias} = ${type};`);
5364
+ .map(([name, type]) => ({ name, declaration: `type ${name} = ${type};` }));
5365
+ }
5366
+
5367
+ // A method of a handle interface: the member text the declarations print inside
5368
+ // the interface, which its FUNCTION entry signs too.
5369
+ interface HandleMember {
5370
+ fn: PreparedFunction;
5371
+ text: string;
5372
+ slotTypes: SlotTypes;
5373
+ }
5374
+
5375
+ // Each handle interface a module's colon methods declare, sorted by name, with
5376
+ // its methods in group order.
5377
+ function collectHandleInterfaces(
5378
+ namespace: string,
5379
+ handleGroups: ReadonlyMap<string, readonly PreparedFunction[]>,
5380
+ mapType: DefoldTypeMapper,
5381
+ resolver: TableDocResolver,
5382
+ constantTokens: ConstantSlotTokens,
5383
+ urlParameters: UrlParameterTable,
5384
+ bindingEvidence: ReadonlySet<string>,
5385
+ ): { name: string; members: HandleMember[] }[] {
5386
+ return [...handleGroups]
5387
+ .map(([receiver, group]) => ({
5388
+ name: handleInterfaceName(namespace, receiver),
5389
+ members: group.map((fn): HandleMember => {
5390
+ const slotTypes: Record<string, SlotType> = {};
5391
+ const text = emitMethod(
5392
+ fn,
5393
+ mapType,
5394
+ resolver,
5395
+ constantTokens,
5396
+ urlParameters,
5397
+ bindingEvidence,
5398
+ slotTypes,
5399
+ );
5400
+ return { fn, text, slotTypes };
5401
+ }),
5402
+ }))
5403
+ .sort((a, b) => a.name.localeCompare(b.name));
5404
+ }
5405
+
5406
+ // An interface signed on one line, each member line followed by a space.
5407
+ function interfaceSignature(name: string, members: readonly string[]): string {
5408
+ return `interface ${name} { ${members.map((member) => `${member} `).join("")}}`;
4567
5409
  }
4568
5410
 
4569
5411
  /**
@@ -4620,10 +5462,12 @@ export function emitSymbolSignatures(
4620
5462
  const knownConstantFqns = options?.knownConstantFqns;
4621
5463
  const urlParameters = options?.urlParameters ?? [];
4622
5464
  const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
5465
+ const nilRaisingSlots = options?.nilRaisingSlots;
4623
5466
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4624
5467
  const constantTokens = constantSlotTokenResolver(
4625
5468
  module,
4626
5469
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
5470
+ options?.withheldFunctions,
4627
5471
  );
4628
5472
  const resolver = buildTableDocResolver(
4629
5473
  module.functions.map((fn) => ({
@@ -4650,8 +5494,9 @@ export function emitSymbolSignatures(
4650
5494
  const prepared = prepareFunction(fn, prefix);
4651
5495
  if (prepared === null) continue;
4652
5496
  const slotTypes: Record<string, SlotType> = {};
5497
+ const [first = prepared, ...later] = callArms(prepared, nilRaisingSlots, module.structs ?? []);
4653
5498
  const primary = emitFunction(
4654
- prepared,
5499
+ first,
4655
5500
  emitName(prepared.name),
4656
5501
  mapType,
4657
5502
  resolver,
@@ -4661,18 +5506,18 @@ export function emitSymbolSignatures(
4661
5506
  slotTypes,
4662
5507
  );
4663
5508
  const alternative = firstSlotAlternative(prepared, module.namespace);
4664
- const tsSignature =
4665
- alternative === null
4666
- ? primary
4667
- : `${primary}\n${emitFunction(
4668
- alternative,
4669
- emitName(prepared.name),
4670
- mapType,
4671
- resolver,
4672
- constantTokens,
4673
- urlParameters,
4674
- bindingEvidence,
4675
- )}`;
5509
+ const arms = [...later, ...(alternative === null ? [] : [alternative])].map((arm) =>
5510
+ emitFunction(
5511
+ arm,
5512
+ emitName(prepared.name),
5513
+ mapType,
5514
+ resolver,
5515
+ constantTokens,
5516
+ urlParameters,
5517
+ bindingEvidence,
5518
+ ),
5519
+ );
5520
+ const tsSignature = [primary, ...arms].join("\n");
4676
5521
  out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
4677
5522
  }
4678
5523
 
@@ -4720,24 +5565,29 @@ export function emitSymbolSignatures(
4720
5565
  });
4721
5566
  }
4722
5567
 
5568
+ // A handle interface signs as one line, as a struct does; each member is the
5569
+ // text its method's FUNCTION entry signs.
4723
5570
  const handleGroups = collectHandleMethodGroups(module);
4724
- for (const group of handleGroups.values()) {
4725
- for (const prepared of group) {
4726
- const slotTypes: Record<string, SlotType> = {};
4727
- out.push({
4728
- identity: fnIdentity(prepared.original),
4729
- tsSignature: emitMethod(
4730
- prepared,
4731
- mapType,
4732
- resolver,
4733
- constantTokens,
4734
- urlParameters,
4735
- bindingEvidence,
4736
- slotTypes,
4737
- ),
4738
- slotTypes,
4739
- });
5571
+ for (const { name, members } of collectHandleInterfaces(
5572
+ module.namespace,
5573
+ handleGroups,
5574
+ mapType,
5575
+ resolver,
5576
+ constantTokens,
5577
+ urlParameters,
5578
+ bindingEvidence,
5579
+ )) {
5580
+ for (const { fn, text, slotTypes } of members) {
5581
+ out.push({ identity: fnIdentity(fn.original), tsSignature: text, slotTypes });
4740
5582
  }
5583
+ out.push({
5584
+ identity: { namespace: module.namespace, kind: "TYPEDEF", name, signature: "" },
5585
+ tsSignature: interfaceSignature(
5586
+ name,
5587
+ members.map((member) => member.text),
5588
+ ),
5589
+ slotTypes: NO_SLOTS,
5590
+ });
4741
5591
  }
4742
5592
 
4743
5593
  const constants: PreparedConstant[] = [];
@@ -4766,7 +5616,7 @@ export function emitSymbolSignatures(
4766
5616
  // carries the first, which is verbatim in the `.d.ts` and is what the `/api`
4767
5617
  // render layer reduces to `<placeholder>: <type>`.
4768
5618
  for (const p of module.properties) {
4769
- const [firstMember = ""] = emitPropertyMembers(p, mapType, module.namespace);
5619
+ const [firstMember = ""] = emitPropertyMembers(p, mapType, module.namespace, bindingEvidence);
4770
5620
  out.push({
4771
5621
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
4772
5622
  tsSignature: firstMember,
@@ -4776,13 +5626,14 @@ export function emitSymbolSignatures(
4776
5626
 
4777
5627
  // A struct signs as its interface on one line; each member line is verbatim in
4778
5628
  // the declarations.
4779
- for (const declaration of collectTypeDeclarations(
5629
+ const typeDeclarations = collectTypeDeclarations(
4780
5630
  module,
4781
- new Set(handleGroups.keys()),
5631
+ handleTypedefNames(module.namespace, handleGroups.keys()),
4782
5632
  rawMapType,
4783
5633
  mapType,
4784
5634
  bindingEvidence,
4785
- )) {
5635
+ );
5636
+ for (const declaration of typeDeclarations) {
4786
5637
  out.push({
4787
5638
  identity: {
4788
5639
  namespace: module.namespace,
@@ -4795,23 +5646,50 @@ export function emitSymbolSignatures(
4795
5646
  });
4796
5647
  }
4797
5648
 
5649
+ // An overload-slot alias is declared exactly where the declarations carry it,
5650
+ // so it signs from the same functions and the same declared set.
5651
+ for (const { name, declaration } of overloadSlotAliasDeclarations(
5652
+ [...module.functions, ...(options?.withheldFunctions ?? [])],
5653
+ typeDeclarations,
5654
+ prefix,
5655
+ mapType,
5656
+ constantTokens,
5657
+ urlParameters,
5658
+ bindingEvidence,
5659
+ )) {
5660
+ out.push({
5661
+ identity: { namespace: module.namespace, kind: "TYPEDEF", name, signature: "" },
5662
+ tsSignature: declaration,
5663
+ slotTypes: NO_SLOTS,
5664
+ });
5665
+ }
5666
+
4798
5667
  // The module's constant-union aliases travel as typedefs too: a signature that
4799
5668
  // names one carries no member list of its own, so the docs read the members
4800
5669
  // from here rather than from the emitter's table.
4801
- for (const row of CONSTANT_UNION_ALIASES.filter((r) => r.home === module.namespace)) {
5670
+ const enumAliases = collectEnumAliases(module, constants, nestedRoot);
5671
+ for (const row of ownedConstantUnionAliases(
5672
+ module.namespace,
5673
+ new Set(enumAliases.all.map((alias) => alias.path)),
5674
+ )) {
5675
+ const name = engineTypeName(row.name);
4802
5676
  out.push({
4803
- identity: { namespace: module.namespace, kind: "TYPEDEF", name: row.name, signature: "" },
4804
- tsSignature: unionAliasDeclaration(row.name, row.members),
5677
+ identity: { namespace: module.namespace, kind: "TYPEDEF", name, signature: "" },
5678
+ tsSignature: unionAliasDeclaration(name, row.members),
4805
5679
  slotTypes: NO_SLOTS,
4806
5680
  });
4807
5681
  }
4808
5682
 
4809
- // An enum alias is keyed by its local path (`STATUS`, `ui.COLOR`), as the
4810
- // constant-union aliases above are keyed by their local name.
4811
- const enumAliases = collectEnumAliases(module, constants, nestedRoot);
5683
+ // An enum alias is keyed by its declared local path (`Status`, `ui.Color`),
5684
+ // as the constant-union aliases above are keyed by their declared name.
4812
5685
  for (const alias of enumAliases.all) {
4813
5686
  out.push({
4814
- identity: { namespace: module.namespace, kind: "TYPEDEF", name: alias.path, signature: "" },
5687
+ identity: {
5688
+ namespace: module.namespace,
5689
+ kind: "TYPEDEF",
5690
+ name: renamedLeaf(alias.path),
5691
+ signature: "",
5692
+ },
4815
5693
  tsSignature: unionAliasDeclaration(alias.name, alias.members, alias.nilable),
4816
5694
  slotTypes: NO_SLOTS,
4817
5695
  });
@@ -4829,6 +5707,12 @@ interface PreparedConstant {
4829
5707
  interface PreparedFunction {
4830
5708
  name: string;
4831
5709
  original: ApiFunction;
5710
+ // Parameter positions declared required whatever their doc says: the
5711
+ // nil-raising slots an arm of `nilRaisingArms` takes.
5712
+ required?: ReadonlySet<number>;
5713
+ // A rewrite of the mapped type at a parameter position: the fields an arm of
5714
+ // `presenceArms` requires.
5715
+ retype?: ReadonlyMap<number, (type: string) => string>;
4832
5716
  }
4833
5717
 
4834
5718
  interface PreparedVariable {
@@ -4847,8 +5731,9 @@ interface NestedGroup {
4847
5731
  children: Map<string, NestedGroup>;
4848
5732
  }
4849
5733
 
4850
- // One `ENUM`'s union alias: `name` is the declared identifier, `path` the local
4851
- // path under the module namespace (`ui.COLOR`), `members` the member FQNs.
5734
+ // One `ENUM`'s union alias: `name` is the declared identifier (`Color`), `path`
5735
+ // the reference's local path under the module namespace (`ui.COLOR`), `members`
5736
+ // the member FQNs.
4852
5737
  interface EnumAlias {
4853
5738
  name: string;
4854
5739
  path: string;
@@ -4977,12 +5862,14 @@ function collectEnumAliases(
4977
5862
  const nilable = new Set(module.constants.filter((c) => c.nilable).map((c) => c.name));
4978
5863
  const topLevel: EnumAlias[] = [];
4979
5864
  const all: EnumAlias[] = [];
4980
- const enums = [...(module.enums ?? [])].sort((a, b) => a.name.localeCompare(b.name));
5865
+ const enums = [...(module.enums ?? [])].sort((a, b) =>
5866
+ renamedLeaf(a.name).localeCompare(renamedLeaf(b.name)),
5867
+ );
4981
5868
  for (const e of enums) {
4982
5869
  if (e.members.length === 0 || !e.members.every((m) => declared.has(m))) continue;
4983
5870
  const path = stripPrefix(e.name, prefix);
4984
5871
  if (TS_IDENTIFIER.test(path)) {
4985
- const alias = { name: path, path, members: e.members, nilable };
5872
+ const alias = { name: engineTypeName(path), path, members: e.members, nilable };
4986
5873
  topLevel.push(alias);
4987
5874
  all.push(alias);
4988
5875
  continue;
@@ -4998,7 +5885,7 @@ function collectEnumAliases(
4998
5885
  level = group.children;
4999
5886
  }
5000
5887
  if (group === undefined) continue;
5001
- const alias = { name, path, members: e.members, nilable };
5888
+ const alias = { name: engineTypeName(name), path, members: e.members, nilable };
5002
5889
  group.enumAliases.push(alias);
5003
5890
  all.push(alias);
5004
5891
  }
@@ -5112,10 +5999,13 @@ export const NIL_CONSTANTS: ReadonlyMap<string, string> = new Map([
5112
5999
  // `undocumented-constant-provenance.test.ts` reds an entry no committed target
5113
6000
  // still needs. `sameValueAs` names the documented constant the engine registers
5114
6001
  // with the same value; where the surface declares it, the back-fill takes its
5115
- // type, so a value compares equal to either spelling.
6002
+ // type, so a value compares equal to either spelling. `memberOf` names the
6003
+ // ref-doc `ENUM` the engine returns the constant through; the back-fill joins it
6004
+ // only on a surface whose ref-doc declares that enum, so an older surface
6005
+ // without one keeps a plain constant.
5116
6006
  export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
5117
6007
  string,
5118
- { readonly doc: string; readonly sameValueAs?: string }
6008
+ { readonly doc: string; readonly sameValueAs?: string; readonly memberOf?: string }
5119
6009
  > = new Map([
5120
6010
  [
5121
6011
  "render.CONTEXT_EVENT_CONTEXT_LOST",
@@ -5136,6 +6026,13 @@ export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
5136
6026
  sameValueAs: "window.WINDOW_EVENT_ICONIFIED",
5137
6027
  },
5138
6028
  ],
6029
+ [
6030
+ "liveupdate.LIVEUPDATE_NOT_INITIALIZED",
6031
+ {
6032
+ doc: "Live update is not initialized. `liveupdate.add_mount` and `liveupdate.remove_mount` return it while live update is disabled.",
6033
+ memberOf: "liveupdate.LIVEUPDATE",
6034
+ },
6035
+ ],
5139
6036
  ]);
5140
6037
 
5141
6038
  function withUndocumentedConstants(
@@ -5145,7 +6042,8 @@ function withUndocumentedConstants(
5145
6042
  const prefix = `${module.namespace}.`;
5146
6043
  const documented = new Set(module.constants.map((c) => c.name));
5147
6044
  const backfill: ApiConstant[] = [];
5148
- for (const [fqn, { doc, sameValueAs }] of UNDOCUMENTED_CONSTANTS) {
6045
+ const joined = new Map<string, string[]>();
6046
+ for (const [fqn, { doc, sameValueAs, memberOf }] of UNDOCUMENTED_CONSTANTS) {
5149
6047
  if (!fqn.startsWith(prefix) || fqn.slice(prefix.length).includes(".")) continue;
5150
6048
  if (documented.has(fqn)) continue;
5151
6049
  if (registered !== undefined && !registered.has(fqn)) continue;
@@ -5157,10 +6055,18 @@ function withUndocumentedConstants(
5157
6055
  description: "",
5158
6056
  ...(aliasOf === undefined ? {} : { aliasOf }),
5159
6057
  });
6058
+ if (memberOf !== undefined) joined.set(memberOf, [...(joined.get(memberOf) ?? []), fqn]);
5160
6059
  }
5161
- return backfill.length === 0
5162
- ? module
5163
- : { ...module, constants: [...module.constants, ...backfill] };
6060
+ if (backfill.length === 0) return module;
6061
+ const enums = module.enums?.map((e) => {
6062
+ const members = joined.get(e.name);
6063
+ return members === undefined ? e : { ...e, members: [...e.members, ...members] };
6064
+ });
6065
+ return {
6066
+ ...module,
6067
+ constants: [...module.constants, ...backfill],
6068
+ ...(enums === undefined ? {} : { enums }),
6069
+ };
5164
6070
  }
5165
6071
 
5166
6072
  // `constant` is the module's own element for `fqn`, absent for a constant another
@@ -5244,10 +6150,26 @@ function memberSignature(
5244
6150
  ): string {
5245
6151
  const original = prepared.original.parameters;
5246
6152
  const elementName = prepared.original.name;
5247
- const cutoff = trailingOptionalCutoff(original, elementName);
6153
+ const required = prepared.required ?? new Set<number>();
6154
+ const cutoff = Math.max(
6155
+ trailingOptionalCutoff(original, elementName),
6156
+ ...[...required].map((index) => index + 1),
6157
+ );
5248
6158
  const varargIndex = original.findIndex(isVarargParameter);
5249
6159
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
5250
- emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
6160
+ emitParameter(
6161
+ p,
6162
+ i,
6163
+ i >= cutoff,
6164
+ mapType,
6165
+ resolver,
6166
+ constantTokens,
6167
+ elementName,
6168
+ urlParameters,
6169
+ bindingEvidence,
6170
+ required.has(i),
6171
+ prepared.retype?.get(i),
6172
+ ),
5251
6173
  );
5252
6174
  const rest =
5253
6175
  varargIndex === -1
@@ -5260,6 +6182,7 @@ function memberSignature(
5260
6182
  constantTokens,
5261
6183
  elementName,
5262
6184
  urlParameters,
6185
+ bindingEvidence,
5263
6186
  );
5264
6187
  const params = [...positional.map((p) => p.text), ...(rest ? [rest.text] : [])].join(", ");
5265
6188
  const ret = emitReturn(
@@ -5296,7 +6219,7 @@ function memberSignature(
5296
6219
  if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
5297
6220
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
5298
6221
  }
5299
- return `${name}(${params}): ${ret.type};${ret.trailing}`;
6222
+ return `${name}${box2dBackendTypeParameters(prepared.original)}(${params}): ${ret.type};${ret.trailing}`;
5300
6223
  }
5301
6224
 
5302
6225
  function emitFunction(
@@ -5367,6 +6290,7 @@ function functionDocLines(
5367
6290
  };
5368
6291
  });
5369
6292
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
6293
+ const sparseNote = SPARSE_ARRAY_RETURNS.get(fn.name)?.note;
5370
6294
  const notedReturns =
5371
6295
  multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
5372
6296
  const lua = htmlToCodeText(fn.examples ?? "");
@@ -5399,7 +6323,10 @@ function functionDocLines(
5399
6323
  ? {
5400
6324
  returns: appendNotes(
5401
6325
  translateProseFences(htmlToDocText(onlyReturn.doc), fn.name, translations),
5402
- baseNotes(fn.name, "return", onlyReturn.name),
6326
+ [
6327
+ ...baseNotes(fn.name, "return", onlyReturn.name),
6328
+ ...(sparseNote === undefined ? [] : [sparseNote]),
6329
+ ],
5403
6330
  ),
5404
6331
  }
5405
6332
  : notedReturns !== undefined
@@ -5521,6 +6448,7 @@ function parameterType(
5521
6448
  constantTokens: ConstantSlotTokens,
5522
6449
  elementName: string,
5523
6450
  urlParameters: UrlParameterTable,
6451
+ bindingEvidence: ReadonlySet<string>,
5524
6452
  ): string {
5525
6453
  const concrete = p.types.filter((t) => t !== "nil");
5526
6454
  // The caller supplies a parameter, so a string-keyed table in it also takes an
@@ -5542,10 +6470,19 @@ function parameterType(
5542
6470
  elementName,
5543
6471
  "param",
5544
6472
  p.name,
6473
+ bindingEvidence,
5545
6474
  )
5546
6475
  : "unknown";
5547
- const correction = PARAM_TYPE_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name));
5548
- if (correction === undefined || !pinMatches(p.types, correctionPins(correction))) return mapped;
6476
+ const key = tableSlotKey(elementName, "param", p.name);
6477
+ const correction = PARAM_TYPE_CORRECTIONS.get(key);
6478
+ if (
6479
+ correction === undefined ||
6480
+ !pinMatches(p.types, correctionPins(correction)) ||
6481
+ (correction.binding !== undefined && !bindingEvidence.has(key))
6482
+ ) {
6483
+ return mapped;
6484
+ }
6485
+ if (correction.ts !== undefined) return correction.ts;
5549
6486
  const kept =
5550
6487
  correction.removes === undefined ? mapped : withoutUnionMembers(mapped, correction.removes);
5551
6488
  if (correction.adds === undefined) return kept;
@@ -5627,13 +6564,22 @@ function emitRestParameter(
5627
6564
  constantTokens: ConstantSlotTokens,
5628
6565
  elementName: string,
5629
6566
  urlParameters: UrlParameterTable,
6567
+ bindingEvidence: ReadonlySet<string>,
5630
6568
  ): { text: string; slots: EmittedSlot[] } {
5631
6569
  const vararg = params[varargIndex];
5632
6570
  if (vararg === undefined) return { text: "", slots: [] };
5633
6571
  const folded = params.slice(varargIndex).map((p, offset) => ({
5634
6572
  position: varargIndex + offset,
5635
6573
  name: p.name,
5636
- ts: parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
6574
+ ts: parameterType(
6575
+ p,
6576
+ mapType,
6577
+ resolver,
6578
+ constantTokens,
6579
+ elementName,
6580
+ urlParameters,
6581
+ bindingEvidence,
6582
+ ),
5637
6583
  }));
5638
6584
  const members = [...new Set(folded.map((slot) => slot.ts))];
5639
6585
  const first = members[0] ?? "unknown";
@@ -5661,15 +6607,28 @@ function emitParameter(
5661
6607
  constantTokens: ConstantSlotTokens,
5662
6608
  elementName: string,
5663
6609
  urlParameters: UrlParameterTable,
6610
+ bindingEvidence: ReadonlySet<string>,
6611
+ required = false,
6612
+ retype: (type: string) => string = (type) => type,
5664
6613
  ): { text: string; type: string; optional: boolean } {
5665
6614
  const name = safeParamName(p.name, index);
5666
- const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
6615
+ const ts = retype(
6616
+ parameterType(
6617
+ p,
6618
+ mapType,
6619
+ resolver,
6620
+ constantTokens,
6621
+ elementName,
6622
+ urlParameters,
6623
+ bindingEvidence,
6624
+ ),
6625
+ );
5667
6626
  // An interior doc-optional param (a required param follows, so the trailing-`?`
5668
6627
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
5669
6628
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
5670
6629
  // `?` form; required params are untouched.
5671
6630
  const nilRequired = requiredSlotCorrection(p, elementName)?.nil === true;
5672
- const interiorOptional = !optional && (isDocOptional(p, elementName) || nilRequired);
6631
+ const interiorOptional = !optional && !required && (isDocOptional(p, elementName) || nilRequired);
5673
6632
  // `(a) => void | undefined` reads as a function returning `void | undefined`.
5674
6633
  const operand = interiorOptional && isFunctionTypeText(ts) ? `(${ts})` : ts;
5675
6634
  const type = `${operand}${interiorOptional ? " | undefined" : ""}`;
@@ -5734,6 +6693,7 @@ function emitReturn(
5734
6693
  elementName,
5735
6694
  "return",
5736
6695
  rv.name,
6696
+ bindingEvidence,
5737
6697
  )
5738
6698
  : "unknown"),
5739
6699
  };
@@ -5772,6 +6732,7 @@ function emitReturn(
5772
6732
  elementName,
5773
6733
  "return",
5774
6734
  first.name,
6735
+ bindingEvidence,
5775
6736
  )
5776
6737
  : "unknown";
5777
6738
  // A LuaLS vararg return (`...`) is every value the call returns, however many.
@@ -5811,12 +6772,17 @@ function emitPropertyMembers(
5811
6772
  p: ApiProperty,
5812
6773
  mapType: DefoldTypeMapper,
5813
6774
  namespace: string,
6775
+ bindingEvidence: ReadonlySet<string>,
5814
6776
  ): readonly string[] {
5815
- const keyRange = PROPERTY_KEY_RANGES.get(`${namespace}.${p.name}`);
6777
+ const rangeKey = `${namespace}.${p.name}`;
6778
+ const keyRange = PROPERTY_KEY_RANGES.get(rangeKey);
5816
6779
  const keys: string[] = [];
5817
6780
  if (keyRange !== undefined) {
5818
- for (let unit = keyRange.first; unit <= keyRange.last; unit++)
5819
- keys.push(`${keyRange.prefix}${unit}`);
6781
+ const last =
6782
+ keyRange.engine !== undefined && bindingEvidence.has(rangeKey)
6783
+ ? keyRange.engine.last
6784
+ : keyRange.last;
6785
+ for (let unit = keyRange.first; unit <= last; unit++) keys.push(`${keyRange.prefix}${unit}`);
5820
6786
  } else {
5821
6787
  keys.push(TS_IDENTIFIER.test(p.name) ? p.name : JSON.stringify(p.name));
5822
6788
  }
@@ -5838,6 +6804,48 @@ function emitPropertyMembers(
5838
6804
  return keys.map((key) => `${modifier}${key}: ${ts};`);
5839
6805
  }
5840
6806
 
6807
+ // A function whose returned table the engine binding keys by a sparse integer,
6808
+ // so the sequence its reference declares is a map with holes: `ipairs` stops at
6809
+ // the first hole, and `#t` may return any border. Keyed by function name, since the return slot is named
6810
+ // `table` in 1.13.0/1.13.1 and `textures` from 1.13.2. The return's `T[]` member
6811
+ // emits as `LuaMap<number, T>`, which `for...of` lowers to `pairs`, and `note`
6812
+ // joins its `@returns`.
6813
+ export const SPARSE_ARRAY_RETURNS: ReadonlyMap<string, { note: string; reason: string }> = new Map([
6814
+ [
6815
+ "material.get_textures",
6816
+ {
6817
+ note: "Keyed by sampler slot from 1; a slot with no texture has no entry, so iterate with `for (const [slot, info] of textures)`.",
6818
+ reason:
6819
+ "script_material.cpp:Material_GetTextures pushes key i+1 only for a slot that has a texture, unlike Compute_GetTextures, which packs its keys",
6820
+ },
6821
+ ],
6822
+ ]);
6823
+
6824
+ function sparseArrayMap(ts: string, elementName: string): string {
6825
+ const members = unionMembers(ts);
6826
+ const arrays = members.filter((member) => member.endsWith("[]"));
6827
+ if (arrays.length === 0) {
6828
+ throw new Error(`SPARSE_ARRAY_RETURNS ${elementName}: the return emits no array (${ts})`);
6829
+ }
6830
+ return members
6831
+ .map((member) => {
6832
+ if (!member.endsWith("[]")) return member;
6833
+ const element = member.slice(0, -2);
6834
+ return `LuaMap<number, ${wrappedInParens(element) ? element.slice(1, -1) : element}>`;
6835
+ })
6836
+ .join(" | ");
6837
+ }
6838
+
6839
+ function wrappedInParens(text: string): boolean {
6840
+ if (!text.startsWith("(")) return false;
6841
+ let depth = 0;
6842
+ for (let i = 0; i < text.length; i++) {
6843
+ if (text[i] === "(") depth++;
6844
+ else if (text[i] === ")" && --depth === 0) return i === text.length - 1;
6845
+ }
6846
+ return false;
6847
+ }
6848
+
5841
6849
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
5842
6850
  // `<dl>` field list emits an inline object type instead of the opaque `Record`
5843
6851
  // fallback. Other tokens in the union map unchanged. `optionalFields` marks the
@@ -5853,8 +6861,9 @@ function mapSlotUnion(
5853
6861
  resolver: TableDocResolver,
5854
6862
  constantTokens: ConstantSlotTokens,
5855
6863
  elementName: string,
5856
- slotKind?: "param" | "return",
5857
- slotName?: string,
6864
+ slotKind: "param" | "return" | undefined,
6865
+ slotName: string | undefined,
6866
+ bindingEvidence: ReadonlySet<string>,
5858
6867
  ): string {
5859
6868
  const mapped: string[] = [];
5860
6869
  const seen = new Set<string>();
@@ -5918,6 +6927,10 @@ function mapSlotUnion(
5918
6927
  }
5919
6928
  } else if (element !== undefined) {
5920
6929
  ts = arrayTypeFromTokens(element, mapType);
6930
+ if (curation?.kind === "array" && curation.pairs === true) {
6931
+ const tokens = typeof element === "string" ? [element] : element;
6932
+ ts = `${ts} | LuaPairsIterable<AnyNotNil, ${unionFromTokens(tokens, mapType)}>`;
6933
+ }
5921
6934
  } else if (curation?.kind === "verbatim") {
5922
6935
  ts = curation.ts;
5923
6936
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -5943,6 +6956,7 @@ function mapSlotUnion(
5943
6956
  slotKind,
5944
6957
  slotName,
5945
6958
  parseTableFields(doc, resolver, slotName) ?? [],
6959
+ bindingEvidence,
5946
6960
  ),
5947
6961
  );
5948
6962
  const object = inlineTableType(parsed, mapType, optionalFields);
@@ -5952,7 +6966,13 @@ function mapSlotUnion(
5952
6966
  if (parsed !== null) {
5953
6967
  const added = applyFieldAdditions(elementName, slotKind, slotName, parsed);
5954
6968
  const nested = applyNestedFieldCurations(elementName, slotKind, slotName, added);
5955
- const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
6969
+ const typed = applyFieldTypeOverrides(
6970
+ elementName,
6971
+ slotKind,
6972
+ slotName,
6973
+ nested,
6974
+ bindingEvidence,
6975
+ );
5956
6976
  const required = applyRequiredFieldCorrections(
5957
6977
  elementName,
5958
6978
  slotKind,
@@ -5991,8 +7011,10 @@ function mapSlotUnion(
5991
7011
  // A genuine `unknown` member (an `any` slot, including `any | nil`) absorbs
5992
7012
  // every other union member, so collapse to exactly `unknown` rather than emit
5993
7013
  // a redundant `T | unknown` or `unknown | undefined`.
5994
- if (mapped.includes("unknown")) return "unknown";
5995
- return joinUnionArms(mapped);
7014
+ const joined = mapped.includes("unknown") ? "unknown" : joinUnionArms(mapped);
7015
+ return slotKind === "return" && SPARSE_ARRAY_RETURNS.has(elementName)
7016
+ ? sparseArrayMap(joined, elementName)
7017
+ : joined;
5996
7018
  }
5997
7019
 
5998
7020
  function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
@@ -6120,8 +7142,8 @@ function withoutSubsumedOpaqueArms(mapped: readonly string[]): string[] {
6120
7142
  });
6121
7143
  }
6122
7144
 
6123
- // Joins already-mapped arms, collapsing the branded ones to their alias when
6124
- // they are exactly a `CONSTANT_UNION_ALIASES` member set. The alias takes the
7145
+ // Joins already-mapped arms, collapsing the branded ones to their row's spelling
7146
+ // when they are exactly a `CONSTANT_UNION_ALIASES` member set. The spelling takes the
6125
7147
  // position of the first branded arm, so a mixed union keeps its reading order
6126
7148
  // (`gui.Easing | Vector`). Arms that are not constants are untouched, and a set
6127
7149
  // with no row keeps every arm inline.
@@ -6139,7 +7161,7 @@ function joinUnionArms(mapped: readonly string[]): string {
6139
7161
  for (const arm of arms) {
6140
7162
  if (BRAND_ARM.test(arm)) {
6141
7163
  if (placed) continue;
6142
- out.push(`${alias.home}.${alias.name}`);
7164
+ out.push(constantUnionSpelling(alias));
6143
7165
  placed = true;
6144
7166
  continue;
6145
7167
  }