@defold-typescript/types 0.51.0 → 0.52.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 (130) hide show
  1. package/api-availability.json +4 -58
  2. package/api-signatures.json +878 -1356
  3. package/api-targets.json +15 -5
  4. package/examples/translations.json +4 -56
  5. package/generated/b2d_world.d.ts +10 -10
  6. package/generated/buffer.d.ts +9 -9
  7. package/generated/bullet3d_collision_object.d.ts +1 -1
  8. package/generated/bullet3d_constraint.d.ts +109 -7
  9. package/generated/bullet3d_shape.d.ts +2 -2
  10. package/generated/go.d.ts +114 -220
  11. package/generated/graphics.d.ts +1 -0
  12. package/generated/gui.d.ts +148 -371
  13. package/generated/http.d.ts +2 -2
  14. package/generated/image.d.ts +55 -3
  15. package/generated/kinds/gui-script.d.ts +2 -0
  16. package/generated/kinds/render-script.d.ts +14 -15
  17. package/generated/liveupdate.d.ts +5 -1
  18. package/generated/material.d.ts +3 -3
  19. package/generated/particlefx.d.ts +20 -1
  20. package/generated/physics.d.ts +45 -1
  21. package/generated/render-script-modules/b2d.d.ts +711 -0
  22. package/generated/render-script-modules/bullet3d.d.ts +73 -0
  23. package/generated/render-script-modules/collectionfactory.d.ts +25 -0
  24. package/generated/render-script-modules/collectionproxy.d.ts +39 -0
  25. package/generated/render-script-modules/factory.d.ts +25 -0
  26. package/generated/render-script-modules/go.d.ts +436 -0
  27. package/generated/render-script-modules/label.d.ts +105 -0
  28. package/generated/render-script-modules/model.d.ts +89 -0
  29. package/generated/render-script-modules/particlefx.d.ts +59 -0
  30. package/generated/render-script-modules/physics.d.ts +333 -0
  31. package/generated/render-script-modules/resource.d.ts +1233 -0
  32. package/generated/render-script-modules/sound.d.ts +227 -0
  33. package/generated/render-script-modules/sprite.d.ts +68 -0
  34. package/generated/render-script-modules/tilemap.d.ts +43 -0
  35. package/generated/render.d.ts +134 -8
  36. package/generated/resource.d.ts +31 -10
  37. package/generated/sprite.d.ts +49 -1
  38. package/generated/sys.d.ts +70 -6
  39. package/generated/versions/defold-1.12.4/buffer.d.ts +8 -8
  40. package/generated/versions/defold-1.12.4/go.d.ts +115 -343
  41. package/generated/versions/defold-1.12.4/graphics.d.ts +1 -0
  42. package/generated/versions/defold-1.12.4/gui.d.ts +151 -513
  43. package/generated/versions/defold-1.12.4/http.d.ts +2 -2
  44. package/generated/versions/defold-1.12.4/image.d.ts +82 -2
  45. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  46. package/generated/versions/defold-1.12.4/liveupdate.d.ts +4 -0
  47. package/generated/versions/defold-1.12.4/particlefx.d.ts +22 -1
  48. package/generated/versions/defold-1.12.4/physics.d.ts +45 -1
  49. package/generated/versions/defold-1.12.4/render.d.ts +78 -2
  50. package/generated/versions/defold-1.12.4/resource.d.ts +43 -10
  51. package/generated/versions/defold-1.12.4/sprite.d.ts +58 -1
  52. package/generated/versions/defold-1.12.4/sys.d.ts +101 -6
  53. package/generated/versions/defold-1.12.4/vmath.d.ts +0 -24
  54. package/generated/versions/defold-1.13.0/buffer.d.ts +8 -8
  55. package/generated/versions/defold-1.13.0/go.d.ts +115 -370
  56. package/generated/versions/defold-1.13.0/graphics.d.ts +1 -0
  57. package/generated/versions/defold-1.13.0/gui.d.ts +151 -513
  58. package/generated/versions/defold-1.13.0/http.d.ts +2 -2
  59. package/generated/versions/defold-1.13.0/image.d.ts +82 -2
  60. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  61. package/generated/versions/defold-1.13.0/liveupdate.d.ts +4 -0
  62. package/generated/versions/defold-1.13.0/material.d.ts +3 -1
  63. package/generated/versions/defold-1.13.0/particlefx.d.ts +22 -1
  64. package/generated/versions/defold-1.13.0/physics.d.ts +45 -1
  65. package/generated/versions/defold-1.13.0/render.d.ts +142 -4
  66. package/generated/versions/defold-1.13.0/resource.d.ts +43 -10
  67. package/generated/versions/defold-1.13.0/sprite.d.ts +58 -1
  68. package/generated/versions/defold-1.13.0/sys.d.ts +101 -6
  69. package/generated/versions/defold-1.13.0/vmath.d.ts +0 -24
  70. package/generated/versions/defold-1.13.1/buffer.d.ts +8 -8
  71. package/generated/versions/defold-1.13.1/go.d.ts +115 -370
  72. package/generated/versions/defold-1.13.1/graphics.d.ts +1 -0
  73. package/generated/versions/defold-1.13.1/gui.d.ts +151 -529
  74. package/generated/versions/defold-1.13.1/http.d.ts +2 -2
  75. package/generated/versions/defold-1.13.1/image.d.ts +82 -2
  76. package/generated/versions/defold-1.13.1/index.d.ts +2 -0
  77. package/generated/versions/defold-1.13.1/liveupdate.d.ts +4 -0
  78. package/generated/versions/defold-1.13.1/material.d.ts +3 -1
  79. package/generated/versions/defold-1.13.1/particlefx.d.ts +22 -1
  80. package/generated/versions/defold-1.13.1/physics.d.ts +45 -1
  81. package/generated/versions/defold-1.13.1/render.d.ts +142 -4
  82. package/generated/versions/defold-1.13.1/resource.d.ts +43 -10
  83. package/generated/versions/defold-1.13.1/sprite.d.ts +58 -1
  84. package/generated/versions/defold-1.13.1/sys.d.ts +101 -6
  85. package/generated/versions/defold-1.13.1/vmath.d.ts +0 -24
  86. package/generated/versions/defold-1.13.2/b2d_world.d.ts +10 -10
  87. package/generated/versions/defold-1.13.2/buffer.d.ts +9 -9
  88. package/generated/versions/defold-1.13.2/bullet3d_collision_object.d.ts +1 -1
  89. package/generated/versions/defold-1.13.2/bullet3d_constraint.d.ts +109 -7
  90. package/generated/versions/defold-1.13.2/bullet3d_shape.d.ts +2 -2
  91. package/generated/versions/defold-1.13.2/go.d.ts +114 -220
  92. package/generated/versions/defold-1.13.2/graphics.d.ts +1 -0
  93. package/generated/versions/defold-1.13.2/gui.d.ts +148 -371
  94. package/generated/versions/defold-1.13.2/http.d.ts +2 -2
  95. package/generated/versions/defold-1.13.2/image.d.ts +55 -3
  96. package/generated/versions/defold-1.13.2/index.d.ts +2 -0
  97. package/generated/versions/defold-1.13.2/liveupdate.d.ts +5 -1
  98. package/generated/versions/defold-1.13.2/material.d.ts +3 -3
  99. package/generated/versions/defold-1.13.2/particlefx.d.ts +20 -1
  100. package/generated/versions/defold-1.13.2/physics.d.ts +45 -1
  101. package/generated/versions/defold-1.13.2/render.d.ts +123 -7
  102. package/generated/versions/defold-1.13.2/resource.d.ts +31 -10
  103. package/generated/versions/defold-1.13.2/sprite.d.ts +49 -1
  104. package/generated/versions/defold-1.13.2/sys.d.ts +70 -6
  105. package/generated/versions/defold-1.13.2/vmath.d.ts +0 -24
  106. package/generated/vmath.d.ts +0 -24
  107. package/identity-vocabulary.json +52 -7
  108. package/index.d.ts +2 -0
  109. package/package.json +2 -1
  110. package/scripts/generate-api-availability.ts +1 -21
  111. package/scripts/generate-api-signatures.ts +75 -14
  112. package/scripts/materialize-version.ts +48 -7
  113. package/scripts/regen.ts +214 -19
  114. package/scripts/signature-store-fs.ts +1 -0
  115. package/scripts/target-version.ts +26 -0
  116. package/src/api-doc.ts +26 -0
  117. package/src/component-properties.d.ts +89 -0
  118. package/src/core-types.ts +42 -15
  119. package/src/doc-corrections.ts +53 -5
  120. package/src/emit-dts.ts +758 -198
  121. package/src/engine-globals.d.ts +5 -1
  122. package/src/go-overloads.d.ts +68 -13
  123. package/src/gui-overloads.d.ts +489 -0
  124. package/src/gui-texture-removal.d.ts +41 -0
  125. package/src/index.ts +2 -0
  126. package/src/lifecycle.ts +30 -21
  127. package/src/msg-overloads.d.ts +25 -13
  128. package/src/signature-store.ts +50 -0
  129. package/src/timers.d.ts +2 -1
  130. 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,
@@ -36,6 +40,7 @@ import {
36
40
  type LualsPosition,
37
41
  mapLualsExpression,
38
42
  needsArrayParens,
43
+ splitTopLevel,
39
44
  } from "./luals-type-expr";
40
45
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
41
46
 
@@ -86,8 +91,15 @@ export interface EmitOptions {
86
91
  // applies only when its key is here, so a surface with no vendored bindings
87
92
  // (absent) applies none of them.
88
93
  bindingEvidence?: ReadonlySet<string>;
94
+ // The 1-based parameter slots, keyed by function FQN, that the target's engine
95
+ // bindings let a call omit but raise on when passed nil. Each optional one
96
+ // splits its function into a call without the slot and a call requiring it,
97
+ // so a possibly-undefined value is a compile error. Absent for a surface with
98
+ // no vendored bindings (a ref-doc target), which keeps every `?`.
99
+ nilRaisingSlots?: ReadonlyMap<string, readonly number[]>;
89
100
  // The functions a target's skip rules removed from the module, read only to
90
- // type an `OVERLOAD_SLOT_ALIASES` slot.
101
+ // type an `OVERLOAD_SLOT_ALIASES` slot and to resolve a
102
+ // `CONSTANT_SLOT_RESOLUTIONS` borrow from their docs.
91
103
  withheldFunctions?: readonly ApiFunction[];
92
104
  }
93
105
 
@@ -179,14 +191,12 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
179
191
  // runtime-owned passthrough: the table is filled by user code at runtime, never
180
192
  // by Defold's API contract. factory.create/collectionfactory.create `properties`
181
193
  // 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`.
194
+ // names. No static shape exists, so `Record<string | number, unknown>` is
195
+ // faithful, not a loss. collectionfactory.create curates the instance-id layer
196
+ // of `properties` as a mapping; each per-instance property table stays that
197
+ // faithful `Record`.
187
198
  "factory.create",
188
199
  "collectionfactory.create",
189
- "on_message",
190
200
  // engine-formatted blob: the engine produces the value at runtime and no
191
201
  // field list is documented, so the whole-slot `Record` is faithful, not a
192
202
  // loss. crash.get_backtrace is "table containing the backtrace",
@@ -259,6 +269,11 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
259
269
  "go.get",
260
270
  "go.set",
261
271
  "go.property",
272
+ // gui-overloads.d.ts closes the node property names and types each value per
273
+ // name, with a hash form for material constants.
274
+ "gui.animate",
275
+ "gui.get",
276
+ "gui.set",
262
277
  // msg-overloads.d.ts supplies the typed msg.post shape.
263
278
  "msg.post",
264
279
  // msg-overloads.d.ts supplies the typed msg.url shape (0 / 1-string / 3-arg
@@ -277,6 +292,7 @@ export const OVERLOAD_COVERED_SKIPS = new Set([
277
292
  "vmath.slerp",
278
293
  "vmath.mul_per_elem",
279
294
  "vmath.normalize",
295
+ "vmath.dot",
280
296
  // sys-overloads.d.ts supplies the disabling form and the form that takes the
281
297
  // cooldown the engine requires when enabling.
282
298
  "sys.set_engine_throttle",
@@ -334,13 +350,10 @@ export const MAPPING_TABLE_SLOTS: ReadonlyMap<string, { key: string; value: stri
334
350
  ]);
335
351
 
336
352
  // 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.
353
+ // empty yet the engine returns a value. Per-FQN, not a blanket "empty
354
+ // returnvalues -> unknown" rule: most functions with empty returnvalues return
355
+ // nothing, and `void` is correct there.
342
356
  export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
343
- ["gui.get", "unknown"],
344
357
  // Upstream documents the returned transaction step in prose only — its
345
358
  // `returnvalues` is empty while every sibling `editor.tx.*` builder declares
346
359
  // one, and `editor.transact()` takes exactly that.
@@ -459,8 +472,8 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
459
472
  slot: "code",
460
473
  },
461
474
  ],
462
- // 1.13.2 declares `render_predicate`, whose TYPEDEF aliases `userdata`, and
463
- // that still emits `number`.
475
+ // 1.13.2 retypes the return to `render_predicate`, whose TYPEDEF aliases
476
+ // `userdata`; its type mapping emits the same brand.
464
477
  [
465
478
  "render.predicate",
466
479
  {
@@ -538,6 +551,20 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
538
551
  binding: tilemapEarlyNil("TileMap_GetTiles"),
539
552
  },
540
553
  ],
554
+ [
555
+ "bullet3d.shape.get_shape",
556
+ {
557
+ ts: "bullet3d.shape.definition | { type: typeof bullet3d.shape.SHAPE_TYPE_MESH }",
558
+ upstream: ["bullet3d.shape.definition"],
559
+ reason:
560
+ "script_bullet3d_shape.cpp:Shape_GetShape sets only `type` for a triangle mesh, which no shape definition arm matches",
561
+ binding: {
562
+ file: "gamesys/src/gamesys/scripts/bullet3d/script_bullet3d_shape.cpp",
563
+ cFunction: "Shape_GetShape",
564
+ contains: "case TRIANGLE_MESH_SHAPE_PROXYTYPE: case GIMPACT_SHAPE_PROXYTYPE: break;",
565
+ },
566
+ },
567
+ ],
541
568
  [
542
569
  "collectionproxy.set_collection",
543
570
  {
@@ -757,6 +784,35 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
757
784
  },
758
785
  ] as const,
759
786
  ),
787
+ ...(
788
+ [
789
+ ["clone_tree", "LuaCloneTree"],
790
+ ["get_tree", "LuaGetTree"],
791
+ ] as const
792
+ ).map(
793
+ ([name, cFunction]) =>
794
+ [
795
+ `gui.${name}:param:node`,
796
+ {
797
+ adds: "undefined",
798
+ upstream: ["node"],
799
+ 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`,
800
+ },
801
+ ] as const,
802
+ ),
803
+ ...(["get_slider_motor", "set_slider_motor"] as const).map(
804
+ (name) =>
805
+ [
806
+ `bullet3d.constraint.${name}:param:motor`,
807
+ {
808
+ adds: '"linear" | "angular"',
809
+ removes: ["string"],
810
+ upstream: ["string"],
811
+ reason:
812
+ "script_bullet3d_constraint.cpp:CheckSliderMotorAxis raises \"motor must be 'linear' or 'angular'\" on any other string",
813
+ },
814
+ ] as const,
815
+ ),
760
816
  [
761
817
  "sound.get_group_name:param:group",
762
818
  {
@@ -835,9 +891,10 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
835
891
  "render.disable_texture:param:binding",
836
892
  {
837
893
  adds: "number",
894
+ removes: ['Opaque<"texture">'],
838
895
  upstream: ["texture", "string", "hash"],
839
896
  reason:
840
- '"texture binding, either by texture unit, string or hash"; example `render.disable_texture(0)`',
897
+ "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
898
  },
842
899
  ],
843
900
  [
@@ -855,11 +912,42 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
855
912
  adds: 'Opaque<"render_predicate">',
856
913
  removes: ["number"],
857
914
  upstream: ["number"],
858
- retypedUpstream: [["render_predicate"]],
859
915
  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`",
916
+ "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
917
  },
862
918
  ],
919
+ ...(["enable_state", "disable_state"] as const).map(
920
+ (name) =>
921
+ [
922
+ `render.${name}:param:state`,
923
+ {
924
+ adds: "graphics.State",
925
+ removes: ["graphics.STATE"],
926
+ upstream: ["graphics.STATE"],
927
+ 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`,
928
+ },
929
+ ] as const,
930
+ ),
931
+ ...(
932
+ [
933
+ "source_factor_color",
934
+ "destination_factor_color",
935
+ "source_factor_alpha",
936
+ "destination_factor_alpha",
937
+ ] as const
938
+ ).map(
939
+ (slot) =>
940
+ [
941
+ `render.set_blend_func_separate:param:${slot}`,
942
+ {
943
+ adds: "graphics.BlendFactor",
944
+ removes: ["number"],
945
+ upstream: ["number"],
946
+ reason:
947
+ 'render_script.cpp:RenderScript_SetBlendFuncSeparate raises "Invalid blend factor" on any value CheckBlendFactor does not list',
948
+ },
949
+ ] as const,
950
+ ),
863
951
  [
864
952
  "go.delete:param:id",
865
953
  {
@@ -878,15 +966,6 @@ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> =
878
966
  reason: 'sys.load_buffer example `buffer.get_bytes(my_buffer, "data")`',
879
967
  },
880
968
  ],
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
969
  [
891
970
  "gui.set_rotation:param:rotation",
892
971
  {
@@ -1095,6 +1174,10 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
1095
1174
  "render.set_render_target:param:render_target",
1096
1175
  "render_script.cpp:RenderScript_SetRenderTarget sets the default target when lua_gettop(L) == 0",
1097
1176
  ],
1177
+ [
1178
+ "buffer.get_bytes:param:stream_name",
1179
+ "script_buffer.cpp:GetBytes reads only argument 1 with CheckBufferUnpack and returns every byte of the buffer",
1180
+ ],
1098
1181
  ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
1099
1182
  ...(
1100
1183
  [
@@ -1237,13 +1320,6 @@ export const REQUIRED_SLOT_CORRECTIONS: ReadonlyMap<string, RequiredSlotCorrecti
1237
1320
  evidence: "gui_script.cpp:LuaSetParent reads a node from argument 2 unless lua_isnil(L, 2)",
1238
1321
  },
1239
1322
  ],
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
1323
  [
1248
1324
  "window.set_listener:param:callback",
1249
1325
  {
@@ -1333,6 +1409,335 @@ function firstSlotAlternative(
1333
1409
  return original === null ? null : { ...prepared, original };
1334
1410
  }
1335
1411
 
1412
+ // The calls a function splits into at its optional nil-raising slots
1413
+ // (`EmitOptions.nilRaisingSlots`): one ending before each such slot, then one
1414
+ // taking them all, every such slot before an arm's end declared required. A
1415
+ // required slot, or one a rest parameter folds in, splits nothing.
1416
+ function nilRaisingArms(
1417
+ prepared: PreparedFunction,
1418
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]>,
1419
+ ): PreparedFunction[] {
1420
+ const { name, parameters } = prepared.original;
1421
+ const firstVararg = parameters.findIndex(isVarargParameter);
1422
+ const positional = firstVararg === -1 ? parameters.length : firstVararg;
1423
+ const split = [...new Set(nilRaisingSlots.get(name) ?? [])]
1424
+ .map((slot) => slot - 1)
1425
+ .filter((index) => {
1426
+ const p = parameters[index];
1427
+ return index < positional && p !== undefined && isDocOptional(p, name);
1428
+ })
1429
+ .sort((a, b) => a - b);
1430
+ if (split.length === 0) return [prepared];
1431
+ const shorter = split.map((end, k) => ({
1432
+ ...prepared,
1433
+ original: { ...prepared.original, parameters: parameters.slice(0, end) },
1434
+ required: new Set(split.slice(0, k)),
1435
+ }));
1436
+ return [...shorter, { ...prepared, required: new Set(split) }];
1437
+ }
1438
+
1439
+ // Table fields a binding reads as required only when `argument` is non-nil,
1440
+ // keyed by FQN. A curated function splits into a call whose `table` slot requires
1441
+ // `fields`, with `argument` as declared, then a call whose `argument` is nil, so
1442
+ // a possibly-nil argument must carry them.
1443
+ export interface PresenceRequiredFields {
1444
+ readonly argument: string;
1445
+ readonly table: string;
1446
+ readonly fields: readonly string[];
1447
+ readonly reason: string;
1448
+ }
1449
+
1450
+ const FRAME_B_FIELDS = ["frame_b_position", "frame_b_rotation"] as const;
1451
+
1452
+ export const PRESENCE_REQUIRED_FIELDS: ReadonlyMap<string, PresenceRequiredFields> = new Map<
1453
+ string,
1454
+ PresenceRequiredFields
1455
+ >([
1456
+ [
1457
+ "bullet3d.constraint.create_point_to_point",
1458
+ {
1459
+ argument: "body_b",
1460
+ table: "params",
1461
+ fields: ["pivot_b"],
1462
+ reason:
1463
+ "script_bullet3d_constraint.cpp:Constraint_CreatePointToPoint reads pivot_b as required when input.m_BodyB != 0",
1464
+ },
1465
+ ],
1466
+ ...(
1467
+ [
1468
+ ["create_hinge", "Constraint_CreateHinge"],
1469
+ ["create_cone_twist", "Constraint_CreateConeTwist"],
1470
+ ["create_generic_6dof", "Constraint_CreateGeneric6Dof"],
1471
+ ["create_slider", "Constraint_CreateSlider"],
1472
+ ] as const
1473
+ ).map(
1474
+ ([name, cFunction]) =>
1475
+ [
1476
+ `bullet3d.constraint.${name}`,
1477
+ {
1478
+ argument: "body_b",
1479
+ table: "params",
1480
+ fields: FRAME_B_FIELDS,
1481
+ reason: `script_bullet3d_constraint.cpp:${cFunction} reads frame B as required under if (input.m_BodyB)`,
1482
+ },
1483
+ ] as const,
1484
+ ),
1485
+ ]);
1486
+
1487
+ // The calls PRESENCE_REQUIRED_FIELDS splits `prepared` into: the declared one
1488
+ // with its fields required, then the one whose argument is nil.
1489
+ function presenceArms(
1490
+ prepared: PreparedFunction,
1491
+ structs: readonly ApiStruct[],
1492
+ ): PreparedFunction[] {
1493
+ const { name, parameters } = prepared.original;
1494
+ const entry = PRESENCE_REQUIRED_FIELDS.get(name);
1495
+ if (entry === undefined) return [prepared];
1496
+ const argument = parameters.findIndex((p) => p.name === entry.argument);
1497
+ const table = parameters.findIndex((p) => p.name === entry.table);
1498
+ const struct = structs.find((s) => parameters[table]?.types.includes(s.name));
1499
+ if (argument === -1 || struct === undefined) {
1500
+ throw new Error(
1501
+ `presence-required fields ${name}: no ${entry.argument} parameter or ${entry.table} table`,
1502
+ );
1503
+ }
1504
+ const missing = entry.fields.filter((field) => !struct.members.some((m) => m.name === field));
1505
+ if (missing.length > 0) {
1506
+ throw new Error(`presence-required fields ${name}: ${struct.name} lacks ${missing.join(", ")}`);
1507
+ }
1508
+ const picked = entry.fields.map((field) => `"${field}"`).join(" | ");
1509
+ const requireFields = (type: string): string => {
1510
+ const operand = /^[\w$.]+$/.test(type) ? type : `(${type})`;
1511
+ return `${operand} & Required<Pick<${operand}, ${picked}>>`;
1512
+ };
1513
+ const strict: PreparedFunction = { ...prepared, retype: new Map([[table, requireFields]]) };
1514
+ // Declared required, the nil argument emits as `undefined` alone rather than
1515
+ // gaining the `| undefined` of an interior optional slot.
1516
+ const absent: PreparedFunction = {
1517
+ ...prepared,
1518
+ required: new Set([...(prepared.required ?? []), argument]),
1519
+ retype: new Map([[argument, () => "undefined"]]),
1520
+ };
1521
+ return [strict, absent];
1522
+ }
1523
+
1524
+ // A rule an engine binding enforces on one parameter that no ref-doc token
1525
+ // states, keyed `<fqn>:param:<name>`. Each rewrites the slot's mapped type, so
1526
+ // one entry holds on every target whatever token spelling produced that type.
1527
+ export type EngineSlotRule =
1528
+ // `E[]` becomes `[E, ...E[]]`: the binding raises on an empty list.
1529
+ | { readonly kind: "non-empty"; readonly reason: string }
1530
+ // The constants the binding raises on leave the slot's type.
1531
+ | { readonly kind: "excludes"; readonly constants: readonly string[]; readonly reason: string }
1532
+ // With `flag` true the table must carry `fields`, which the binding then reads
1533
+ // without a default.
1534
+ | {
1535
+ readonly kind: "flag-requires";
1536
+ readonly flag: string;
1537
+ readonly fields: readonly string[];
1538
+ readonly reason: string;
1539
+ }
1540
+ // In each call that requires `slot`, this slot is required too and its
1541
+ // top-level union loses `drops`.
1542
+ | {
1543
+ readonly kind: "alongside";
1544
+ readonly slot: string;
1545
+ readonly drops: readonly string[];
1546
+ readonly reason: string;
1547
+ };
1548
+
1549
+ export const ENGINE_SLOT_RULES: ReadonlyMap<string, EngineSlotRule> = new Map<
1550
+ string,
1551
+ EngineSlotRule
1552
+ >([
1553
+ [
1554
+ "buffer.create:param:declaration",
1555
+ {
1556
+ kind: "non-empty",
1557
+ reason:
1558
+ 'script_buffer.cpp:Create raises "You must specify at least one stream declaration" when the list holds fewer than one entry',
1559
+ },
1560
+ ],
1561
+ [
1562
+ "buffer.set_metadata:param:values",
1563
+ {
1564
+ kind: "non-empty",
1565
+ reason:
1566
+ 'script_buffer.cpp:SetMetadata raises "invalid metadata" when lua_objlen of values is 0',
1567
+ },
1568
+ ],
1569
+ [
1570
+ "buffer.set_metadata:param:value_type",
1571
+ {
1572
+ kind: "excludes",
1573
+ constants: ["buffer.VALUE_TYPE_INT64", "buffer.VALUE_TYPE_UINT64"],
1574
+ reason:
1575
+ 'script_buffer.cpp:SetMetadata raises "64 bit integer metadata are not supported." on VALUE_TYPE_UINT64 and VALUE_TYPE_INT64',
1576
+ },
1577
+ ],
1578
+ [
1579
+ "resource.create_sound_data:param:options",
1580
+ {
1581
+ kind: "flag-requires",
1582
+ flag: "partial",
1583
+ fields: ["filesize"],
1584
+ reason:
1585
+ "script_resource.cpp:CreateSoundData reads filesize with the no-default CheckFieldValue<int> when partial is true, and CheckValue<int> raises on a non-number",
1586
+ },
1587
+ ],
1588
+ [
1589
+ "go.delete:param:id",
1590
+ {
1591
+ kind: "alongside",
1592
+ slot: "recursive",
1593
+ drops: ["boolean"],
1594
+ reason:
1595
+ "gameobject_script.cpp:Script_Delete raises on a boolean id with a second argument, and on a nil id once recursive is popped",
1596
+ },
1597
+ ],
1598
+ ]);
1599
+
1600
+ // `prepared` with `rewrite` applied after any rewrite it already makes at `index`.
1601
+ function withRetype(
1602
+ prepared: PreparedFunction,
1603
+ index: number,
1604
+ rewrite: (type: string) => string,
1605
+ ): PreparedFunction {
1606
+ const retype = new Map(prepared.retype);
1607
+ const prior = retype.get(index);
1608
+ retype.set(index, prior === undefined ? rewrite : (type) => rewrite(prior(type)));
1609
+ return { ...prepared, retype };
1610
+ }
1611
+
1612
+ // `E[]` as `[E, ...E[]]`; the element keeps its own parentheses.
1613
+ function nonEmptyList(type: string, key: string): string {
1614
+ const element = type.endsWith("[]") ? type.slice(0, -2) : "";
1615
+ const single =
1616
+ splitTopLevel(type, "|").length === 1 &&
1617
+ splitTopLevel(type, "&").length === 1 &&
1618
+ !isFunctionTypeText(type);
1619
+ if (element === "" || !single) {
1620
+ throw new Error(`engine slot rule ${key}: ${type} is not one array`);
1621
+ }
1622
+ return `[${element}, ...${element}[]]`;
1623
+ }
1624
+
1625
+ function withoutMembers(type: string, drops: readonly string[], key: string): string {
1626
+ const members = splitTopLevel(type, "|").map((member) => member.trim());
1627
+ const missing = drops.filter((drop) => !members.includes(drop));
1628
+ if (missing.length > 0) {
1629
+ throw new Error(`engine slot rule ${key}: ${type} has no ${missing.join(", ")} to drop`);
1630
+ }
1631
+ return members.filter((member) => !drops.includes(member)).join(" | ");
1632
+ }
1633
+
1634
+ // The field names a table slot declares: its STRUCT's members, else the fields
1635
+ // parsed from its own doc.
1636
+ function slotFieldNames(parameter: ApiParameter, structs: readonly ApiStruct[]): string[] {
1637
+ const struct = structs.find((s) => parameter.types.includes(s.name));
1638
+ if (struct !== undefined) return struct.members.map((member) => member.name);
1639
+ const fields = parameter.fields ?? parseTableFields(parameter.doc) ?? [];
1640
+ return fields.map((field) => field.name);
1641
+ }
1642
+
1643
+ function engineSlotRuleArms(
1644
+ prepared: PreparedFunction,
1645
+ arms: readonly PreparedFunction[],
1646
+ key: string,
1647
+ rule: EngineSlotRule,
1648
+ index: number,
1649
+ structs: readonly ApiStruct[],
1650
+ bindingsRead: boolean,
1651
+ ): PreparedFunction[] {
1652
+ const { name, parameters } = prepared.original;
1653
+ const takes = (arm: PreparedFunction): boolean => index < arm.original.parameters.length;
1654
+ const rewriteEach = (rewrite: (type: string) => string): PreparedFunction[] =>
1655
+ arms.map((arm) => (takes(arm) ? withRetype(arm, index, rewrite) : arm));
1656
+ switch (rule.kind) {
1657
+ case "non-empty":
1658
+ return rewriteEach((type) => nonEmptyList(type, key));
1659
+ case "excludes": {
1660
+ const excluded = rule.constants.map((constant) => `typeof ${constant}`).join(" | ");
1661
+ return rewriteEach((type) => `Exclude<${type}, ${excluded}>`);
1662
+ }
1663
+ case "flag-requires": {
1664
+ const declared = slotFieldNames(parameters[index] as ApiParameter, structs);
1665
+ const missing = [rule.flag, ...rule.fields].filter((field) => !declared.includes(field));
1666
+ if (missing.length > 0) {
1667
+ throw new Error(`engine slot rule ${key}: the table declares no ${missing.join(", ")}`);
1668
+ }
1669
+ const picked = rule.fields.map((field) => `"${field}"`).join(" | ");
1670
+ return rewriteEach((type) => {
1671
+ const operand =
1672
+ splitTopLevel(type, "|").length > 1 || isFunctionTypeText(type) ? `(${type})` : type;
1673
+ return `${operand} & ({ ${rule.flag}?: false } | ({ ${rule.flag}: true } & Required<Pick<${type}, ${picked}>>))`;
1674
+ });
1675
+ }
1676
+ case "alongside": {
1677
+ const partner = parameters.findIndex((p) => p.name === rule.slot);
1678
+ const partnerParameter = parameters[partner];
1679
+ const requires = (arm: PreparedFunction): boolean =>
1680
+ partnerParameter !== undefined &&
1681
+ partner < arm.original.parameters.length &&
1682
+ (arm.required?.has(partner) === true || !isDocOptional(partnerParameter, name));
1683
+ if (!arms.some(requires)) {
1684
+ // Without the target's nil-raising slots no call was split out to
1685
+ // require the partner, so there is no binding to have stopped marking it.
1686
+ if (!bindingsRead) return [...arms];
1687
+ throw new Error(
1688
+ partnerParameter === undefined
1689
+ ? `engine slot rule ${key}: ${name} has no ${rule.slot} parameter`
1690
+ : `engine slot rule ${key}: no call of ${name} requires ${rule.slot}`,
1691
+ );
1692
+ }
1693
+ return arms.map((arm) =>
1694
+ requires(arm)
1695
+ ? {
1696
+ ...withRetype(arm, index, (type) => withoutMembers(type, rule.drops, key)),
1697
+ required: new Set([...(arm.required ?? []), index]),
1698
+ }
1699
+ : arm,
1700
+ );
1701
+ }
1702
+ }
1703
+ }
1704
+
1705
+ // `arms` with every ENGINE_SLOT_RULES entry keyed to `prepared` applied.
1706
+ // `bindingsRead` says the emit was given the target's nil-raising slots.
1707
+ function applyEngineSlotRules(
1708
+ prepared: PreparedFunction,
1709
+ arms: readonly PreparedFunction[],
1710
+ structs: readonly ApiStruct[],
1711
+ bindingsRead: boolean,
1712
+ ): PreparedFunction[] {
1713
+ const { name, parameters } = prepared.original;
1714
+ const prefix = `${name}:param:`;
1715
+ let ruled = [...arms];
1716
+ for (const [key, rule] of ENGINE_SLOT_RULES) {
1717
+ if (!key.startsWith(prefix)) continue;
1718
+ const slot = key.slice(prefix.length);
1719
+ const index = parameters.findIndex((p) => p.name === slot);
1720
+ if (index === -1) {
1721
+ throw new Error(`engine slot rule ${key}: ${name} has no ${slot} parameter`);
1722
+ }
1723
+ ruled = engineSlotRuleArms(prepared, ruled, key, rule, index, structs, bindingsRead);
1724
+ }
1725
+ return ruled;
1726
+ }
1727
+
1728
+ // Every call a function declares, in order: its nil-raising arms, each split
1729
+ // at its presence-required fields, then narrowed by its engine slot rules.
1730
+ function callArms(
1731
+ prepared: PreparedFunction,
1732
+ nilRaisingSlots: ReadonlyMap<string, readonly number[]> | undefined,
1733
+ structs: readonly ApiStruct[],
1734
+ ): PreparedFunction[] {
1735
+ const arms = nilRaisingArms(prepared, nilRaisingSlots ?? new Map()).flatMap((arm) =>
1736
+ presenceArms(arm, structs),
1737
+ );
1738
+ return applyEngineSlotRules(prepared, arms, structs, nilRaisingSlots !== undefined);
1739
+ }
1740
+
1336
1741
  export type ConstantSlotResolution =
1337
1742
  | { readonly borrow: string }
1338
1743
  | { readonly family: string; readonly evidence: string }
@@ -1347,8 +1752,6 @@ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResoluti
1347
1752
  string,
1348
1753
  ConstantSlotResolution
1349
1754
  >([
1350
- ["gui.get:param:property", { borrow: "gui.animate:param:property" }],
1351
- ["gui.set:param:property", { borrow: "gui.animate:param:property" }],
1352
1755
  ["gui.cancel_animations:param:property", { borrow: "gui.animate:param:property" }],
1353
1756
  [
1354
1757
  "buffer.set_metadata:param:value_type",
@@ -1477,6 +1880,29 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1477
1880
  "go.PLAYBACK_LOOP_PINGPONG",
1478
1881
  ],
1479
1882
  },
1883
+ // The factors render_script.cpp:CheckBlendFactor accepts, in its order. Every
1884
+ // target defines the constants; only the newer ones name the union.
1885
+ {
1886
+ home: "graphics",
1887
+ name: "BlendFactor",
1888
+ members: [
1889
+ "graphics.BLEND_FACTOR_ZERO",
1890
+ "graphics.BLEND_FACTOR_ONE",
1891
+ "graphics.BLEND_FACTOR_SRC_COLOR",
1892
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_COLOR",
1893
+ "graphics.BLEND_FACTOR_DST_COLOR",
1894
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_COLOR",
1895
+ "graphics.BLEND_FACTOR_SRC_ALPHA",
1896
+ "graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA",
1897
+ "graphics.BLEND_FACTOR_DST_ALPHA",
1898
+ "graphics.BLEND_FACTOR_ONE_MINUS_DST_ALPHA",
1899
+ "graphics.BLEND_FACTOR_SRC_ALPHA_SATURATE",
1900
+ "graphics.BLEND_FACTOR_CONSTANT_COLOR",
1901
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR",
1902
+ "graphics.BLEND_FACTOR_CONSTANT_ALPHA",
1903
+ "graphics.BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA",
1904
+ ],
1905
+ },
1480
1906
  {
1481
1907
  home: "graphics",
1482
1908
  name: "BufferType",
@@ -1690,6 +2116,12 @@ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
1690
2116
  "graphics.BUFFER_TYPE_STENCIL_BIT",
1691
2117
  ],
1692
2118
  },
2119
+ // The render-target creator raises on any other texture type.
2120
+ {
2121
+ home: "render",
2122
+ name: "RenderTargetTextureType",
2123
+ members: ["graphics.TEXTURE_TYPE_2D", "graphics.TEXTURE_TYPE_CUBE_MAP"],
2124
+ },
1693
2125
  {
1694
2126
  home: "sys",
1695
2127
  name: "NetworkConnectivity",
@@ -1801,9 +2233,14 @@ function constantSlotTokens(
1801
2233
  return documentedConstantTokens(entry.family, universe);
1802
2234
  }
1803
2235
 
1804
- function slotDocLookup(module: ApiModule): SlotDocLookup {
2236
+ // A borrow reads the ref-doc, so a slot of a function the skip rules withheld
2237
+ // (`gui.animate`, which gui-overloads.d.ts declares) still lends its doc.
2238
+ function slotDocLookup(
2239
+ module: ApiModule,
2240
+ withheldFunctions: readonly ApiFunction[] = [],
2241
+ ): SlotDocLookup {
1805
2242
  const docs = new Map<string, string>();
1806
- for (const fn of module.functions) {
2243
+ for (const fn of [...module.functions, ...withheldFunctions]) {
1807
2244
  for (const p of fn.parameters) {
1808
2245
  const key = tableSlotKey(fn.name, "param", p.name);
1809
2246
  if (!docs.has(key)) docs.set(key, p.doc);
@@ -1823,8 +2260,9 @@ export function resolveConstantSlotTokens(
1823
2260
  module: ApiModule,
1824
2261
  slotKey: string,
1825
2262
  universe: ReadonlySet<string>,
2263
+ withheldFunctions: readonly ApiFunction[] = [],
1826
2264
  ): string[] {
1827
- const lookupDoc = slotDocLookup(module);
2265
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1828
2266
  return constantSlotTokens(lookupDoc(slotKey) ?? "", slotKey, lookupDoc, universe);
1829
2267
  }
1830
2268
 
@@ -1838,21 +2276,23 @@ type ConstantSlotTokens = (
1838
2276
  function constantSlotTokenResolver(
1839
2277
  module: ApiModule,
1840
2278
  universe: ReadonlySet<string>,
2279
+ withheldFunctions: readonly ApiFunction[] = [],
1841
2280
  ): ConstantSlotTokens {
1842
- const lookupDoc = slotDocLookup(module);
2281
+ const lookupDoc = slotDocLookup(module, withheldFunctions);
1843
2282
  return (elementName, slotKind, slotName, doc) =>
1844
2283
  constantSlotTokens(doc, tableSlotKey(elementName, slotKind, slotName), lookupDoc, universe);
1845
2284
  }
1846
2285
 
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.
2286
+ // FQN-keyed allowlist of the checks that genuinely narrow their argument, mapped
2287
+ // to the `DEFOLD_TYPE_MAP` token whose type they prove: the `types.is_*` checks,
2288
+ // and `bullet3d.collision_object.is_rigid_body`, whose binding is a
2289
+ // `btRigidBody::upcast` test. Emitting these as user-defined type guards
2290
+ // (`var_ is Vector3`) is the only way to narrow an engine userdata value, which
2291
+ // `typeof` cannot. An allowlist, not a blanket `is_*` match: `b2d.body.is_active`,
2292
+ // `gui.is_enabled`, `sound.is_music_playing`, `go.exists`, `sys.exists` are
2293
+ // runtime-state / existence checks on already-typed arguments and must stay
2294
+ // `boolean`. An unlisted future `types.is_*` also stays a visible `boolean` until
2295
+ // curated here, matching the ARBITRARY_TABLE_SLOTS deliberate-opt-in precedent.
1856
2296
  export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1857
2297
  ["types.is_hash", "hash"],
1858
2298
  ["types.is_matrix4", "matrix4"],
@@ -1861,13 +2301,47 @@ export const TYPE_PREDICATES: ReadonlyMap<string, string> = new Map([
1861
2301
  ["types.is_vector", "vector"],
1862
2302
  ["types.is_vector3", "vector3"],
1863
2303
  ["types.is_vector4", "vector4"],
2304
+ ["bullet3d.collision_object.is_rigid_body", "btRigidBody"],
1864
2305
  ]);
1865
2306
 
2307
+ // The merged ref-doc gives each Box2D backend's form of these queries one
2308
+ // parameter list, so the first (v2) arm always wins and a v3 hit is unreachable.
2309
+ // A type parameter tells them apart: v2/script_box2d_world_v2.cpp:PushCastHit
2310
+ // sets `fixture` and `shape` to one PushFixtureInfo table, while
2311
+ // v3/script_box2d_world_v3.cpp:PushCastHit sets `shape` alone from
2312
+ // PushShapeInfoForShape. The v2 arm is declared first, so a call with no type
2313
+ // argument keeps the v2 hits; its `= "v2"` default only states that. Keyed by
2314
+ // `box2dBackendArmKey`.
2315
+ export const BOX2D_BACKEND_ARMS: ReadonlyMap<string, Box2dBackend> = new Map<string, Box2dBackend>([
2316
+ ["b2d.world.cast_ray:b2d.fixture_cast_hit[]", "v2"],
2317
+ ["b2d.world.cast_ray:b2d.shape_cast_hit[]", "v3"],
2318
+ ["b2d.world.cast_ray_closest:b2d.fixture_cast_hit", "v2"],
2319
+ ["b2d.world.cast_ray_closest:b2d.shape_cast_hit", "v3"],
2320
+ ["b2d.world.cast_shape:b2d.fixture_cast_hit[]", "v2"],
2321
+ ["b2d.world.cast_shape:b2d.shape_cast_hit[]", "v3"],
2322
+ ["b2d.world.overlap_aabb:b2d.fixture_info[]", "v2"],
2323
+ ["b2d.world.overlap_aabb:b2d.shape_info[]", "v3"],
2324
+ ["b2d.world.overlap_shape:b2d.fixture_info[]", "v2"],
2325
+ ["b2d.world.overlap_shape:b2d.shape_info[]", "v3"],
2326
+ ]);
2327
+
2328
+ // `<fqn>:<first non-nil type of the first return>`: the two arms of each
2329
+ // `BOX2D_BACKEND_ARMS` query share every parameter and differ only there.
2330
+ export function box2dBackendArmKey(fn: ApiFunction): string {
2331
+ const first = fn.returnValues[0]?.types.find((type) => type !== "nil") ?? "";
2332
+ return `${fn.name}:${first}`;
2333
+ }
2334
+
2335
+ function box2dBackendTypeParameters(fn: ApiFunction): string {
2336
+ const backend = BOX2D_BACKEND_ARMS.get(box2dBackendArmKey(fn));
2337
+ if (backend === undefined) return "";
2338
+ return backend === "v2" ? '<Backend extends "v2" = "v2">' : '<Backend extends "v3">';
2339
+ }
2340
+
1866
2341
  // Element names whose `table` slot is a prose-only `array/list/table of <T>` shape
1867
2342
  // the field-list parser cannot read, but whose element type a human curated from
1868
2343
  // 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
2344
+ // element is itself a union, emitted `(A | B)[]`. Emitted as a plain
1871
2345
  // array, never `LuaMap`: an array element carries no illegal-index-key problem so
1872
2346
  // no TSTL wiring is needed. The audit consults this map too, so the gate and the
1873
2347
  // emitted surface stay coupled; a slot not curated here still surfaces under
@@ -1882,7 +2356,6 @@ export const HOMOGENEOUS_ARRAY_SLOTS: ReadonlyMap<string, string | readonly stri
1882
2356
  ["vmath.vector", "number"],
1883
2357
  ["sound.get_groups", "hash"],
1884
2358
  ["iap.list", "string"],
1885
- ["go.delete", ["string", "hash", "url"]],
1886
2359
  // push.register's `notifications` is a prose-only array of push.NOTIFICATION_*
1887
2360
  // bitmask constants (the ref example sums NOTIFICATION_BADGE/SOUND/ALERT). The
1888
2361
  // vendored push_doc.json fixture declares no NOTIFICATION_* constant elements, so
@@ -1905,7 +2378,11 @@ export type NestedMapping = { key: string; value: string };
1905
2378
  // for `collectionfactory.create`'s properties.
1906
2379
  export type TableSlotCuration = (
1907
2380
  | { kind: "mapping"; key: string; value: string | readonly TableField[] | NestedMapping }
1908
- | { kind: "array"; element: string | readonly string[] }
2381
+ // `pairs` also admits any table whose values are the element, for a binding
2382
+ // that walks the table with lua_next and reads only its values. The extra arm
2383
+ // is `LuaPairsIterable`, which is covariant in its value; `LuaTable` is
2384
+ // invariant through `set`, so it would reject a `LuaTable<Hash, Hash>`.
2385
+ | { kind: "array"; element: string | readonly string[]; pairs?: true }
1909
2386
  | { kind: "object"; fields: readonly TableField[] }
1910
2387
  | { kind: "array-object"; fields: readonly TableField[] }
1911
2388
  // A `<key> -> <args table>` slot: the doc's `<dl>` describes the *value* shape,
@@ -2082,25 +2559,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2082
2559
  string,
2083
2560
  TableSlotCuration
2084
2561
  >([
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
2562
  // buffer.create's `declaration` is "a table where each entry (table) describes
2105
2563
  // a stream", and upstream's example passes a list of stream records whose
2106
2564
  // `type` is a buffer.VALUE_TYPE_* constant.
@@ -2128,6 +2586,19 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2128
2586
  },
2129
2587
  ],
2130
2588
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
2589
+ // go.delete's "table of id's": gameobject_script.cpp:DeleteGOTable walks any
2590
+ // table with lua_next and resolves each value with ResolveURL, so the id table
2591
+ // collectionfactory.create returns deletes as it is. 1.13.2 retypes the slot
2592
+ // to an id array, which still rejects that table.
2593
+ [
2594
+ "go.delete:param:id",
2595
+ {
2596
+ kind: "array",
2597
+ element: ["string", "hash", "url"],
2598
+ pairs: true,
2599
+ retypedUpstream: ["(string|hash|url)[]"],
2600
+ },
2601
+ ],
2131
2602
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
2132
2603
  // `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
2133
2604
  // hash`). parseTableFields flattens the inner `<dl>` to the top level, which
@@ -2385,62 +2856,6 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2385
2856
  value: "number | vector4",
2386
2857
  },
2387
2858
  ],
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
- ],
2442
- },
2443
- ],
2444
2859
  // physics.create_joint and physics.set_joint_properties both declare
2445
2860
  // `properties` as a joint-type-specific table; the only universal field is
2446
2861
  // `collide_connected` (parseTableFields already recovers it from the
@@ -2932,7 +3347,9 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
2932
3347
  // no `status`; typing it as its own shape needs a per-target authored overload,
2933
3348
  // so `status` stays required and the slot's `note` says how to tell the two
2934
3349
  // apart. Its typed form is the same signature: the typed reference names
2935
- // `http.response`, which the runtime surface withholds.
3350
+ // `http.response`, which the runtime surface withholds. Both forms add the
3351
+ // `sender` no reference lists: comp_script.cpp delivers the response as a
3352
+ // message, with `PushURL(L, message->m_Sender)` then `PCall(L, 4, 0)`.
2936
3353
  // `model.play_anim`'s completion callback receives self, the message id and the
2937
3354
  // message only: script_model.cpp calls it with `PCall(L, 3, 0)`, so both forms
2938
3355
  // drop the `sender` every reference lists and type the message as the typed
@@ -2979,7 +3396,7 @@ export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, CallbackSignature
2979
3396
  [
2980
3397
  "http.request:param:callback",
2981
3398
  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",
3399
+ "(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
3400
  "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
3401
  ),
2985
3402
  ],
@@ -3064,11 +3481,11 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
3064
3481
  ["resource.set_atlas:param:table:animations[].playback", "go.Playback"],
3065
3482
  ["resource.create_atlas:param:table:animations[].playback", "go.Playback"],
3066
3483
  ["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
3067
- ["gui.set:param:options:key", "string | Hash"],
3068
3484
  ["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
3069
3485
  ["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.
3486
+ // The option bags 1.13.2 types as a STRUCT, whose members still carry the
3487
+ // narrower upstream types: `gui.set`'s (its slot is declared in
3488
+ // gui-overloads.d.ts) and `resource.create_sound_data`'s.
3072
3489
  ["gui.set_options:member:key", "string | Hash"],
3073
3490
  ["resource.sound_data_options:member:data", 'string | Opaque<"buffer">'],
3074
3491
  // Bitmasks typed as their flag enum: the create_texture example passes
@@ -3083,6 +3500,11 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
3083
3500
  ] as const
3084
3501
  ).map((struct) => [`${struct}:member:flags`, "graphics.TEXTURE_USAGE_FLAG | number"] as const),
3085
3502
  ["render.render_target_buffer_params:member:flags", "render.RENDER_TARGET_FLAG | number"],
3503
+ // gamesys_script.cpp:PushSampler, behind both get_samplers, pushes the raw
3504
+ // texture type; NormalizeBindableTextureType never runs there. A separate
3505
+ // sampler reports TEXTURE_TYPE_SAMPLER (6) and a separate texture binding one
3506
+ // of the four TEXTURE_TYPE_TEXTURE_* (7-10), none of which has a Lua constant.
3507
+ ["material.sampler_info:member:type", "graphics.TEXTURE_TYPE | 6 | 7 | 8 | 9 | 10"],
3086
3508
  ]);
3087
3509
 
3088
3510
  // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
@@ -3102,13 +3524,6 @@ export interface TableFieldAddition {
3102
3524
  readonly evidence: readonly string[];
3103
3525
  }
3104
3526
 
3105
- const PROPERTY_OPTION_KEYS: TableField = {
3106
- name: "keys",
3107
- types: ["table"],
3108
- optional: true,
3109
- tsType: "(Hash | string)[]",
3110
- };
3111
-
3112
3527
  export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map<
3113
3528
  string,
3114
3529
  TableFieldAddition
@@ -3123,24 +3538,6 @@ export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition>
3123
3538
  ],
3124
3539
  },
3125
3540
  ],
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
3541
  ]);
3145
3542
 
3146
3543
  // A return-slot field upstream documents as platform-gated or nil-valued while
@@ -3890,6 +4287,58 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
3890
4287
  return grouped;
3891
4288
  }
3892
4289
 
4290
+ // A TYPEDEF whose aliased type the engine binding contradicts, replaced verbatim.
4291
+ // Member overrides reach only a STRUCT, and these alias inline LuaLS tables.
4292
+ // Each applies only while the typedef still aliases exactly its `upstream` tokens,
4293
+ // in order, so a changed upstream typedef is emitted as written rather than
4294
+ // shadowed; `typedef-correction-provenance.test.ts` pins every entry against the
4295
+ // retained ref-docs. Keyed by the typedef's ref-doc name.
4296
+ export interface TypedefAliasCorrection {
4297
+ readonly upstream: readonly string[];
4298
+ readonly ts: string;
4299
+ readonly reason: string;
4300
+ }
4301
+
4302
+ // One arm per shape a script can build, in upstream's order, each keyed by its
4303
+ // own shape-type constant.
4304
+ const BULLET3D_SHAPE_ARMS = [
4305
+ ["SPHERE", "diameter: number"],
4306
+ ["BOX", "dimensions: Vector3"],
4307
+ ["CAPSULE", "diameter: number; height: number"],
4308
+ ["HULL", "vertices: Vector3[]"],
4309
+ ] as const;
4310
+
4311
+ export const TYPEDEF_ALIAS_CORRECTIONS: ReadonlyMap<string, TypedefAliasCorrection> = new Map([
4312
+ [
4313
+ "render.render_target_params",
4314
+ {
4315
+ upstream: [
4316
+ "{ type?:graphics.TEXTURE_TYPE, sample_count?:integer, [graphics.BUFFER_TYPE]:render.render_target_buffer_params }",
4317
+ ],
4318
+ ts: "{ type?: render.RenderTargetTextureType; sample_count?: number } & { [key: number]: render.render_target_buffer_params }",
4319
+ reason:
4320
+ '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',
4321
+ },
4322
+ ],
4323
+ [
4324
+ "bullet3d.shape.definition",
4325
+ {
4326
+ upstream: [
4327
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4328
+ "{ type:bullet3d.shape.SHAPE_TYPE, dimensions:vector3, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4329
+ "{ type:bullet3d.shape.SHAPE_TYPE, diameter:number, height:number, position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4330
+ "{ type:bullet3d.shape.SHAPE_TYPE, vertices:vector3[], position?:vector3, rotation?:quaternion, target_rotation?:quaternion }",
4331
+ ],
4332
+ ts: BULLET3D_SHAPE_ARMS.map(
4333
+ ([kind, fields]) =>
4334
+ `{ type: typeof bullet3d.shape.SHAPE_TYPE_${kind}; ${fields}; position?: Vector3; rotation?: Quaternion; target_rotation?: Quaternion }`,
4335
+ ).join(" | "),
4336
+ reason:
4337
+ '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',
4338
+ },
4339
+ ],
4340
+ ]);
4341
+
3893
4342
  // A handle the type map widens (`constant_buffer` takes named constants) keeps
3894
4343
  // that shape, so annotating with the alias loses nothing the handle-returning
3895
4344
  // function yields.
@@ -3906,6 +4355,14 @@ function typedefAlias(
3906
4355
  ): string {
3907
4356
  const brand = `Opaque<"${typedef.name}">`;
3908
4357
  const aliasOf = typedef.aliasOf ?? [];
4358
+ const correction = TYPEDEF_ALIAS_CORRECTIONS.get(typedef.name);
4359
+ if (
4360
+ correction !== undefined &&
4361
+ aliasOf.length === correction.upstream.length &&
4362
+ aliasOf.every((token, i) => token === correction.upstream[i])
4363
+ ) {
4364
+ return `type ${local} = ${correction.ts};`;
4365
+ }
3909
4366
  if (aliasOf.length > 0 && !isHandleAlias(aliasOf)) {
3910
4367
  const armType: DefoldTypeMapper = DISCRIMINATED_MESSAGE_UNIONS.has(typedef.name)
3911
4368
  ? (token, position) => discriminatedMessageArm(token) ?? mapType(token, position)
@@ -4248,11 +4705,13 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4248
4705
  const translations = options?.translations ?? {};
4249
4706
  const urlParameters = options?.urlParameters ?? [];
4250
4707
  const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
4708
+ const nilRaisingSlots = options?.nilRaisingSlots;
4251
4709
  const baseNotes = indexBaseNoteResolver(options?.indexBaseSource ?? "engine");
4252
4710
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4253
4711
  const constantTokens = constantSlotTokenResolver(
4254
4712
  module,
4255
4713
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
4714
+ options?.withheldFunctions,
4256
4715
  );
4257
4716
 
4258
4717
  const constants = module.constants
@@ -4385,18 +4844,20 @@ export function emitDeclarations(documented: ApiModule, options?: EmitOptions):
4385
4844
  for (const fn of functions) {
4386
4845
  const reserved = TS_RESERVED_NAMES.has(fn.name);
4387
4846
  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}`);
4847
+ for (const arm of callArms(fn, nilRaisingSlots, module.structs ?? [])) {
4848
+ for (const docLine of functionDocLines(fn.original, translations, baseNotes))
4849
+ lines.push(docLine);
4850
+ const line = emitFunction(
4851
+ arm,
4852
+ emitName,
4853
+ mapType,
4854
+ resolver,
4855
+ constantTokens,
4856
+ urlParameters,
4857
+ bindingEvidence,
4858
+ );
4859
+ lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
4860
+ }
4400
4861
  const alternative = firstSlotAlternative(fn, module.namespace);
4401
4862
  if (alternative !== null) {
4402
4863
  // The overload documents its own parameters, so its hover names the base
@@ -4620,10 +5081,12 @@ export function emitSymbolSignatures(
4620
5081
  const knownConstantFqns = options?.knownConstantFqns;
4621
5082
  const urlParameters = options?.urlParameters ?? [];
4622
5083
  const bindingEvidence = options?.bindingEvidence ?? new Set<string>();
5084
+ const nilRaisingSlots = options?.nilRaisingSlots;
4623
5085
  const { rawMapType, mapType } = moduleTypeMappers(module, constantsByFqn, options);
4624
5086
  const constantTokens = constantSlotTokenResolver(
4625
5087
  module,
4626
5088
  new Set([...constantsByFqn.keys(), ...(knownConstantFqns ?? [])]),
5089
+ options?.withheldFunctions,
4627
5090
  );
4628
5091
  const resolver = buildTableDocResolver(
4629
5092
  module.functions.map((fn) => ({
@@ -4650,8 +5113,9 @@ export function emitSymbolSignatures(
4650
5113
  const prepared = prepareFunction(fn, prefix);
4651
5114
  if (prepared === null) continue;
4652
5115
  const slotTypes: Record<string, SlotType> = {};
5116
+ const [first = prepared, ...later] = callArms(prepared, nilRaisingSlots, module.structs ?? []);
4653
5117
  const primary = emitFunction(
4654
- prepared,
5118
+ first,
4655
5119
  emitName(prepared.name),
4656
5120
  mapType,
4657
5121
  resolver,
@@ -4661,18 +5125,18 @@ export function emitSymbolSignatures(
4661
5125
  slotTypes,
4662
5126
  );
4663
5127
  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
- )}`;
5128
+ const arms = [...later, ...(alternative === null ? [] : [alternative])].map((arm) =>
5129
+ emitFunction(
5130
+ arm,
5131
+ emitName(prepared.name),
5132
+ mapType,
5133
+ resolver,
5134
+ constantTokens,
5135
+ urlParameters,
5136
+ bindingEvidence,
5137
+ ),
5138
+ );
5139
+ const tsSignature = [primary, ...arms].join("\n");
4676
5140
  out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
4677
5141
  }
4678
5142
 
@@ -4829,6 +5293,12 @@ interface PreparedConstant {
4829
5293
  interface PreparedFunction {
4830
5294
  name: string;
4831
5295
  original: ApiFunction;
5296
+ // Parameter positions declared required whatever their doc says: the
5297
+ // nil-raising slots an arm of `nilRaisingArms` takes.
5298
+ required?: ReadonlySet<number>;
5299
+ // A rewrite of the mapped type at a parameter position: the fields an arm of
5300
+ // `presenceArms` requires.
5301
+ retype?: ReadonlyMap<number, (type: string) => string>;
4832
5302
  }
4833
5303
 
4834
5304
  interface PreparedVariable {
@@ -5112,10 +5582,13 @@ export const NIL_CONSTANTS: ReadonlyMap<string, string> = new Map([
5112
5582
  // `undocumented-constant-provenance.test.ts` reds an entry no committed target
5113
5583
  // still needs. `sameValueAs` names the documented constant the engine registers
5114
5584
  // with the same value; where the surface declares it, the back-fill takes its
5115
- // type, so a value compares equal to either spelling.
5585
+ // type, so a value compares equal to either spelling. `memberOf` names the
5586
+ // ref-doc `ENUM` the engine returns the constant through; the back-fill joins it
5587
+ // only on a surface whose ref-doc declares that enum, so an older surface
5588
+ // without one keeps a plain constant.
5116
5589
  export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
5117
5590
  string,
5118
- { readonly doc: string; readonly sameValueAs?: string }
5591
+ { readonly doc: string; readonly sameValueAs?: string; readonly memberOf?: string }
5119
5592
  > = new Map([
5120
5593
  [
5121
5594
  "render.CONTEXT_EVENT_CONTEXT_LOST",
@@ -5136,6 +5609,13 @@ export const UNDOCUMENTED_CONSTANTS: ReadonlyMap<
5136
5609
  sameValueAs: "window.WINDOW_EVENT_ICONIFIED",
5137
5610
  },
5138
5611
  ],
5612
+ [
5613
+ "liveupdate.LIVEUPDATE_NOT_INITIALIZED",
5614
+ {
5615
+ doc: "Live update is not initialized. `liveupdate.add_mount` and `liveupdate.remove_mount` return it while live update is disabled.",
5616
+ memberOf: "liveupdate.LIVEUPDATE",
5617
+ },
5618
+ ],
5139
5619
  ]);
5140
5620
 
5141
5621
  function withUndocumentedConstants(
@@ -5145,7 +5625,8 @@ function withUndocumentedConstants(
5145
5625
  const prefix = `${module.namespace}.`;
5146
5626
  const documented = new Set(module.constants.map((c) => c.name));
5147
5627
  const backfill: ApiConstant[] = [];
5148
- for (const [fqn, { doc, sameValueAs }] of UNDOCUMENTED_CONSTANTS) {
5628
+ const joined = new Map<string, string[]>();
5629
+ for (const [fqn, { doc, sameValueAs, memberOf }] of UNDOCUMENTED_CONSTANTS) {
5149
5630
  if (!fqn.startsWith(prefix) || fqn.slice(prefix.length).includes(".")) continue;
5150
5631
  if (documented.has(fqn)) continue;
5151
5632
  if (registered !== undefined && !registered.has(fqn)) continue;
@@ -5157,10 +5638,18 @@ function withUndocumentedConstants(
5157
5638
  description: "",
5158
5639
  ...(aliasOf === undefined ? {} : { aliasOf }),
5159
5640
  });
5641
+ if (memberOf !== undefined) joined.set(memberOf, [...(joined.get(memberOf) ?? []), fqn]);
5160
5642
  }
5161
- return backfill.length === 0
5162
- ? module
5163
- : { ...module, constants: [...module.constants, ...backfill] };
5643
+ if (backfill.length === 0) return module;
5644
+ const enums = module.enums?.map((e) => {
5645
+ const members = joined.get(e.name);
5646
+ return members === undefined ? e : { ...e, members: [...e.members, ...members] };
5647
+ });
5648
+ return {
5649
+ ...module,
5650
+ constants: [...module.constants, ...backfill],
5651
+ ...(enums === undefined ? {} : { enums }),
5652
+ };
5164
5653
  }
5165
5654
 
5166
5655
  // `constant` is the module's own element for `fqn`, absent for a constant another
@@ -5244,10 +5733,25 @@ function memberSignature(
5244
5733
  ): string {
5245
5734
  const original = prepared.original.parameters;
5246
5735
  const elementName = prepared.original.name;
5247
- const cutoff = trailingOptionalCutoff(original, elementName);
5736
+ const required = prepared.required ?? new Set<number>();
5737
+ const cutoff = Math.max(
5738
+ trailingOptionalCutoff(original, elementName),
5739
+ ...[...required].map((index) => index + 1),
5740
+ );
5248
5741
  const varargIndex = original.findIndex(isVarargParameter);
5249
5742
  const positional = (varargIndex === -1 ? original : original.slice(0, varargIndex)).map((p, i) =>
5250
- emitParameter(p, i, i >= cutoff, mapType, resolver, constantTokens, elementName, urlParameters),
5743
+ emitParameter(
5744
+ p,
5745
+ i,
5746
+ i >= cutoff,
5747
+ mapType,
5748
+ resolver,
5749
+ constantTokens,
5750
+ elementName,
5751
+ urlParameters,
5752
+ required.has(i),
5753
+ prepared.retype?.get(i),
5754
+ ),
5251
5755
  );
5252
5756
  const rest =
5253
5757
  varargIndex === -1
@@ -5296,7 +5800,7 @@ function memberSignature(
5296
5800
  if (isPredicate && soleParam !== undefined && predicateToken !== undefined) {
5297
5801
  return `${name}(${params}): ${safeParamName(soleParam.name, 0)} is ${mapType(predicateToken)};`;
5298
5802
  }
5299
- return `${name}(${params}): ${ret.type};${ret.trailing}`;
5803
+ return `${name}${box2dBackendTypeParameters(prepared.original)}(${params}): ${ret.type};${ret.trailing}`;
5300
5804
  }
5301
5805
 
5302
5806
  function emitFunction(
@@ -5367,6 +5871,7 @@ function functionDocLines(
5367
5871
  };
5368
5872
  });
5369
5873
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
5874
+ const sparseNote = SPARSE_ARRAY_RETURNS.get(fn.name)?.note;
5370
5875
  const notedReturns =
5371
5876
  multiReturnDoc(fn, translations, baseNotes) ?? overrideReturnDoc(fn, baseNotes);
5372
5877
  const lua = htmlToCodeText(fn.examples ?? "");
@@ -5399,7 +5904,10 @@ function functionDocLines(
5399
5904
  ? {
5400
5905
  returns: appendNotes(
5401
5906
  translateProseFences(htmlToDocText(onlyReturn.doc), fn.name, translations),
5402
- baseNotes(fn.name, "return", onlyReturn.name),
5907
+ [
5908
+ ...baseNotes(fn.name, "return", onlyReturn.name),
5909
+ ...(sparseNote === undefined ? [] : [sparseNote]),
5910
+ ],
5403
5911
  ),
5404
5912
  }
5405
5913
  : notedReturns !== undefined
@@ -5661,15 +6169,19 @@ function emitParameter(
5661
6169
  constantTokens: ConstantSlotTokens,
5662
6170
  elementName: string,
5663
6171
  urlParameters: UrlParameterTable,
6172
+ required = false,
6173
+ retype: (type: string) => string = (type) => type,
5664
6174
  ): { text: string; type: string; optional: boolean } {
5665
6175
  const name = safeParamName(p.name, index);
5666
- const ts = parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters);
6176
+ const ts = retype(
6177
+ parameterType(p, mapType, resolver, constantTokens, elementName, urlParameters),
6178
+ );
5667
6179
  // An interior doc-optional param (a required param follows, so the trailing-`?`
5668
6180
  // projection cannot mark it) keeps its optionality as `| undefined` — TSTL
5669
6181
  // lowers `undefined` to `nil`, the faithful call. Trailing optionals keep the
5670
6182
  // `?` form; required params are untouched.
5671
6183
  const nilRequired = requiredSlotCorrection(p, elementName)?.nil === true;
5672
- const interiorOptional = !optional && (isDocOptional(p, elementName) || nilRequired);
6184
+ const interiorOptional = !optional && !required && (isDocOptional(p, elementName) || nilRequired);
5673
6185
  // `(a) => void | undefined` reads as a function returning `void | undefined`.
5674
6186
  const operand = interiorOptional && isFunctionTypeText(ts) ? `(${ts})` : ts;
5675
6187
  const type = `${operand}${interiorOptional ? " | undefined" : ""}`;
@@ -5838,6 +6350,48 @@ function emitPropertyMembers(
5838
6350
  return keys.map((key) => `${modifier}${key}: ${ts};`);
5839
6351
  }
5840
6352
 
6353
+ // A function whose returned table the engine binding keys by a sparse integer,
6354
+ // so the sequence its reference declares is a map with holes: `ipairs` stops at
6355
+ // the first hole, and `#t` may return any border. Keyed by function name, since the return slot is named
6356
+ // `table` in 1.13.0/1.13.1 and `textures` from 1.13.2. The return's `T[]` member
6357
+ // emits as `LuaMap<number, T>`, which `for...of` lowers to `pairs`, and `note`
6358
+ // joins its `@returns`.
6359
+ export const SPARSE_ARRAY_RETURNS: ReadonlyMap<string, { note: string; reason: string }> = new Map([
6360
+ [
6361
+ "material.get_textures",
6362
+ {
6363
+ note: "Keyed by sampler slot from 1; a slot with no texture has no entry, so iterate with `for (const [slot, info] of textures)`.",
6364
+ reason:
6365
+ "script_material.cpp:Material_GetTextures pushes key i+1 only for a slot that has a texture, unlike Compute_GetTextures, which packs its keys",
6366
+ },
6367
+ ],
6368
+ ]);
6369
+
6370
+ function sparseArrayMap(ts: string, elementName: string): string {
6371
+ const members = unionMembers(ts);
6372
+ const arrays = members.filter((member) => member.endsWith("[]"));
6373
+ if (arrays.length === 0) {
6374
+ throw new Error(`SPARSE_ARRAY_RETURNS ${elementName}: the return emits no array (${ts})`);
6375
+ }
6376
+ return members
6377
+ .map((member) => {
6378
+ if (!member.endsWith("[]")) return member;
6379
+ const element = member.slice(0, -2);
6380
+ return `LuaMap<number, ${wrappedInParens(element) ? element.slice(1, -1) : element}>`;
6381
+ })
6382
+ .join(" | ");
6383
+ }
6384
+
6385
+ function wrappedInParens(text: string): boolean {
6386
+ if (!text.startsWith("(")) return false;
6387
+ let depth = 0;
6388
+ for (let i = 0; i < text.length; i++) {
6389
+ if (text[i] === "(") depth++;
6390
+ else if (text[i] === ")" && --depth === 0) return i === text.length - 1;
6391
+ }
6392
+ return false;
6393
+ }
6394
+
5841
6395
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
5842
6396
  // `<dl>` field list emits an inline object type instead of the opaque `Record`
5843
6397
  // fallback. Other tokens in the union map unchanged. `optionalFields` marks the
@@ -5918,6 +6472,10 @@ function mapSlotUnion(
5918
6472
  }
5919
6473
  } else if (element !== undefined) {
5920
6474
  ts = arrayTypeFromTokens(element, mapType);
6475
+ if (curation?.kind === "array" && curation.pairs === true) {
6476
+ const tokens = typeof element === "string" ? [element] : element;
6477
+ ts = `${ts} | LuaPairsIterable<AnyNotNil, ${unionFromTokens(tokens, mapType)}>`;
6478
+ }
5921
6479
  } else if (curation?.kind === "verbatim") {
5922
6480
  ts = curation.ts;
5923
6481
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -5991,8 +6549,10 @@ function mapSlotUnion(
5991
6549
  // A genuine `unknown` member (an `any` slot, including `any | nil`) absorbs
5992
6550
  // every other union member, so collapse to exactly `unknown` rather than emit
5993
6551
  // a redundant `T | unknown` or `unknown | undefined`.
5994
- if (mapped.includes("unknown")) return "unknown";
5995
- return joinUnionArms(mapped);
6552
+ const joined = mapped.includes("unknown") ? "unknown" : joinUnionArms(mapped);
6553
+ return slotKind === "return" && SPARSE_ARRAY_RETURNS.has(elementName)
6554
+ ? sparseArrayMap(joined, elementName)
6555
+ : joined;
5996
6556
  }
5997
6557
 
5998
6558
  function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {