@defold-typescript/types 0.38.0 → 0.40.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 (109) hide show
  1. package/api-availability.json +117 -0
  2. package/api-signatures.json +586 -480
  3. package/examples/translations.json +356 -180
  4. package/generated/b2d_body.d.ts +7 -4
  5. package/generated/b2d_shape.d.ts +23 -1
  6. package/generated/buffer.d.ts +34 -19
  7. package/generated/camera.d.ts +3 -2
  8. package/generated/collectionfactory.d.ts +17 -12
  9. package/generated/collectionproxy.d.ts +2 -2
  10. package/generated/compute.d.ts +1 -1
  11. package/generated/factory.d.ts +2 -1
  12. package/generated/go.d.ts +150 -91
  13. package/generated/graphics.d.ts +54 -0
  14. package/generated/gui.d.ts +117 -71
  15. package/generated/html5.d.ts +4 -2
  16. package/generated/http.d.ts +8 -2
  17. package/generated/image.d.ts +24 -15
  18. package/generated/kinds/gui-script.d.ts +1 -0
  19. package/generated/kinds/render-script.d.ts +1 -0
  20. package/generated/kinds/script.d.ts +1 -0
  21. package/generated/material.d.ts +1 -1
  22. package/generated/model.d.ts +41 -7
  23. package/generated/particlefx.d.ts +1 -1
  24. package/generated/physics.d.ts +29 -20
  25. package/generated/profiler.d.ts +10 -3
  26. package/generated/render.d.ts +293 -111
  27. package/generated/resource.d.ts +243 -226
  28. package/generated/socket.d.ts +33 -18
  29. package/generated/sound.d.ts +6 -5
  30. package/generated/sprite.d.ts +9 -1
  31. package/generated/sys.d.ts +44 -27
  32. package/generated/tilemap.d.ts +25 -19
  33. package/generated/timer.d.ts +33 -24
  34. package/generated/versions/defold-1.12.4/buffer.d.ts +34 -19
  35. package/generated/versions/defold-1.12.4/camera.d.ts +3 -2
  36. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +17 -12
  37. package/generated/versions/defold-1.12.4/collectionproxy.d.ts +1 -1
  38. package/generated/versions/defold-1.12.4/factory.d.ts +2 -1
  39. package/generated/versions/defold-1.12.4/go.d.ts +150 -91
  40. package/generated/versions/defold-1.12.4/graphics.d.ts +2 -0
  41. package/generated/versions/defold-1.12.4/gui.d.ts +109 -69
  42. package/generated/versions/defold-1.12.4/html5.d.ts +4 -2
  43. package/generated/versions/defold-1.12.4/http.d.ts +8 -2
  44. package/generated/versions/defold-1.12.4/image.d.ts +24 -15
  45. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  46. package/generated/versions/defold-1.12.4/model.d.ts +41 -7
  47. package/generated/versions/defold-1.12.4/particlefx.d.ts +1 -1
  48. package/generated/versions/defold-1.12.4/physics.d.ts +29 -20
  49. package/generated/versions/defold-1.12.4/profiler.d.ts +10 -3
  50. package/generated/versions/defold-1.12.4/render.d.ts +292 -110
  51. package/generated/versions/defold-1.12.4/resource.d.ts +243 -226
  52. package/generated/versions/defold-1.12.4/socket.d.ts +33 -18
  53. package/generated/versions/defold-1.12.4/sound.d.ts +6 -5
  54. package/generated/versions/defold-1.12.4/sprite.d.ts +9 -1
  55. package/generated/versions/defold-1.12.4/sys.d.ts +44 -27
  56. package/generated/versions/defold-1.12.4/tilemap.d.ts +25 -19
  57. package/generated/versions/defold-1.12.4/timer.d.ts +33 -24
  58. package/generated/versions/defold-1.12.4/vmath.d.ts +18 -10
  59. package/generated/versions/defold-1.12.4/window.d.ts +5 -3
  60. package/generated/versions/defold-1.13.0/b2d_body.d.ts +7 -4
  61. package/generated/versions/defold-1.13.0/b2d_shape.d.ts +23 -1
  62. package/generated/versions/defold-1.13.0/buffer.d.ts +34 -19
  63. package/generated/versions/defold-1.13.0/camera.d.ts +3 -2
  64. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +17 -12
  65. package/generated/versions/defold-1.13.0/collectionproxy.d.ts +1 -1
  66. package/generated/versions/defold-1.13.0/compute.d.ts +1 -1
  67. package/generated/versions/defold-1.13.0/factory.d.ts +2 -1
  68. package/generated/versions/defold-1.13.0/go.d.ts +150 -91
  69. package/generated/versions/defold-1.13.0/graphics.d.ts +50 -0
  70. package/generated/versions/defold-1.13.0/gui.d.ts +109 -69
  71. package/generated/versions/defold-1.13.0/html5.d.ts +4 -2
  72. package/generated/versions/defold-1.13.0/http.d.ts +8 -2
  73. package/generated/versions/defold-1.13.0/image.d.ts +24 -15
  74. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  75. package/generated/versions/defold-1.13.0/material.d.ts +1 -1
  76. package/generated/versions/defold-1.13.0/model.d.ts +41 -7
  77. package/generated/versions/defold-1.13.0/particlefx.d.ts +1 -1
  78. package/generated/versions/defold-1.13.0/physics.d.ts +29 -20
  79. package/generated/versions/defold-1.13.0/profiler.d.ts +10 -3
  80. package/generated/versions/defold-1.13.0/render.d.ts +293 -111
  81. package/generated/versions/defold-1.13.0/resource.d.ts +243 -226
  82. package/generated/versions/defold-1.13.0/socket.d.ts +33 -18
  83. package/generated/versions/defold-1.13.0/sound.d.ts +6 -5
  84. package/generated/versions/defold-1.13.0/sprite.d.ts +9 -1
  85. package/generated/versions/defold-1.13.0/sys.d.ts +44 -27
  86. package/generated/versions/defold-1.13.0/tilemap.d.ts +25 -19
  87. package/generated/versions/defold-1.13.0/timer.d.ts +33 -24
  88. package/generated/versions/defold-1.13.0/vmath.d.ts +18 -10
  89. package/generated/versions/defold-1.13.0/window.d.ts +5 -3
  90. package/generated/vmath.d.ts +18 -10
  91. package/generated/window.d.ts +5 -3
  92. package/index.d.ts +1 -0
  93. package/package.json +2 -1
  94. package/scripts/generate-api-signatures.ts +2 -1
  95. package/scripts/prose-constants.ts +96 -0
  96. package/scripts/regen.ts +6 -2
  97. package/src/api-doc.ts +8 -0
  98. package/src/component-properties.d.ts +33 -0
  99. package/src/core-types.ts +4 -2
  100. package/src/custom-messages.d.ts +4 -3
  101. package/src/doc-comment.ts +247 -43
  102. package/src/emit-dts.ts +958 -57
  103. package/src/engine-globals.d.ts +2 -2
  104. package/src/example-store.ts +26 -0
  105. package/src/go-overloads.d.ts +11 -10
  106. package/src/index.ts +9 -0
  107. package/src/param-correction-provenance.ts +67 -0
  108. package/src/render-overloads.d.ts +15 -17
  109. package/src/script-api.ts +3 -1
package/src/emit-dts.ts CHANGED
@@ -13,9 +13,10 @@ import {
13
13
  htmlToCodeText,
14
14
  htmlToDocText,
15
15
  renderDocComment,
16
+ splitExampleSources,
16
17
  } from "./doc-comment";
17
18
  import type { TranslationStore } from "./example-store";
18
- import { hashExampleSource, lookupTranslation } from "./example-store";
19
+ import { hashExampleSource, lookupExampleTranslations, lookupTranslation } from "./example-store";
19
20
  import { classifyUrlParameter, type UrlParameterTable } from "./url-parameters";
20
21
 
21
22
  export interface EmitOptions {
@@ -130,6 +131,8 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
130
131
  // names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
131
132
  // payload (mitigated for authored code by isMessage/onMessage). No static shape
132
133
  // exists, so `Record<string | number, unknown>` is faithful, not a loss.
134
+ // collectionfactory.create curates the instance-id layer of `properties` as a
135
+ // mapping; each per-instance property table stays that faithful `Record`.
133
136
  "factory.create",
134
137
  "collectionfactory.create",
135
138
  "on_message",
@@ -253,6 +256,16 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
253
256
  // ref-doc. Upstream adding `nil` to `types` hands the case to the top-level
254
257
  // `nil` projection in `mapSlotUnion` and reds the entry, which is then deleted
255
258
  // rather than re-pinned.
259
+ //
260
+ // The same table holds a `function` return whose prose documents the returned
261
+ // function's contract, which the bare token cannot carry: `socket.protect`'s
262
+ // safe function "returns nil followed by an error message", and
263
+ // `socket.newtry`'s try function returns its first argument when it is truthy
264
+ // and throws otherwise, the LuaSocket contract its example relies on to unwrap
265
+ // `socket.connect`'s `client, err` pair. The try function takes that pair
266
+ // destructured into separate arguments; a boxed `LuaMultiReturn` is rejected
267
+ // because TSTL passes it as one table, which is always truthy. `reason` quotes
268
+ // the slot prose the provenance test searches for, as for the `nil` entries.
256
269
  export interface ReturnTypeCorrection {
257
270
  readonly ts: string;
258
271
  readonly upstream: readonly string[];
@@ -269,8 +282,193 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
269
282
  reason: "the body if successful. Otherwise nil.",
270
283
  },
271
284
  ],
285
+ [
286
+ "socket.protect",
287
+ {
288
+ ts: "(...args: unknown[]) => LuaMultiReturn<[unknown, string | undefined]>",
289
+ upstream: ["function(function())"],
290
+ reason: "returns nil followed by an error message.",
291
+ },
292
+ ],
293
+ [
294
+ "socket.newtry",
295
+ {
296
+ ts: "<T>(value: T | undefined, ...rest: T extends { readonly __tstlMultiReturn: unknown } ? [boxedMultiReturn: never] : unknown[]) => T",
297
+ upstream: ["function"],
298
+ reason: "the customized try function.",
299
+ },
300
+ ],
272
301
  ]);
273
302
 
303
+ // A parameter whose declared `types` reject what the same element's prose, or
304
+ // an upstream example, demonstrably passes — a render target by resource name,
305
+ // a texture unit, a Lua sequence where `table` is declared. Admission rule: only
306
+ // a slot upstream's own prose or example contradicts; a slot that is merely
307
+ // imprecise is not a candidate. Every such slot is too narrow, never wrong, so
308
+ // `adds` is appended to the mapped union rather than replacing it: the URL
309
+ // address alias, array curation and constant mapping stay live, and optionality
310
+ // stays with `isDocOptional`. Each entry records the upstream tokens it
311
+ // contradicts and `param-correction-provenance.test.ts` pins them against every
312
+ // retained vendored ref-doc. Once no retained target still declares those
313
+ // tokens the entry is deleted, never re-pinned.
314
+ export interface ParamTypeCorrection {
315
+ readonly adds: string;
316
+ readonly upstream: readonly string[];
317
+ readonly reason: string;
318
+ }
319
+
320
+ // Keyed `<element>:param:<slot>` like OPTIONAL_SLOT_CORRECTIONS, so a
321
+ // same-named slot on another element is untouched.
322
+ export const PARAM_TYPE_CORRECTIONS: ReadonlyMap<string, ParamTypeCorrection> = new Map([
323
+ [
324
+ "render.set_render_target:param:render_target",
325
+ {
326
+ adds: "string | Hash | typeof render.RENDER_TARGET_DEFAULT",
327
+ upstream: ["render_target"],
328
+ reason:
329
+ "\"render.RENDER_TARGET_DEFAULT to set the default render target\"; example `render.set_render_target('my_rt_resource')`",
330
+ },
331
+ ],
332
+ [
333
+ "render.get_render_target_width:param:render_target",
334
+ {
335
+ adds: "string | Hash",
336
+ upstream: ["render_target"],
337
+ reason:
338
+ "example `render.get_render_target_width('my_rt_resource', graphics.BUFFER_TYPE_COLOR0_BIT)`",
339
+ },
340
+ ],
341
+ [
342
+ "render.get_render_target_height:param:render_target",
343
+ {
344
+ adds: "string | Hash",
345
+ upstream: ["render_target"],
346
+ reason:
347
+ "example `render.get_render_target_height('my_rt_resource', graphics.BUFFER_TYPE_COLOR0_BIT)`",
348
+ },
349
+ ],
350
+ [
351
+ "render.set_render_target_size:param:render_target",
352
+ {
353
+ adds: "string | Hash",
354
+ upstream: ["render_target"],
355
+ reason:
356
+ "\"a render target created from either a render script, or from a render target resource\"; example `render.set_render_target_size('my_rt_resource', ...)`",
357
+ },
358
+ ],
359
+ [
360
+ "render.enable_texture:param:handle_or_name",
361
+ {
362
+ adds: 'Opaque<"render_target"> | number',
363
+ upstream: ["texture", "string", "hash"],
364
+ reason:
365
+ "\"render target or texture handle that should be bound, or a named resource\"; examples `render.enable_texture(0, self.my_render_target, ...)` and `render.enable_texture(0, 'my_rt_resource', ...)`; `resource.get_texture_info` returns the handle as a number",
366
+ },
367
+ ],
368
+ [
369
+ "render.disable_texture:param:binding",
370
+ {
371
+ adds: "number",
372
+ upstream: ["texture", "string", "hash"],
373
+ reason:
374
+ '"texture binding, either by texture unit, string or hash"; example `render.disable_texture(0)`',
375
+ },
376
+ ],
377
+ [
378
+ "render.set_camera:param:camera",
379
+ {
380
+ adds: "string",
381
+ upstream: ["url", "number", "nil"],
382
+ reason: 'example `render.set_camera("main:/my_go#camera")`',
383
+ },
384
+ ],
385
+ [
386
+ "go.delete:param:id",
387
+ {
388
+ adds: "boolean",
389
+ upstream: ["string", "hash", "url", "table"],
390
+ reason: "example `go.delete(true)` deletes the script game object and its children",
391
+ },
392
+ ],
393
+ [
394
+ "buffer.get_bytes:param:stream_name",
395
+ {
396
+ adds: "string",
397
+ upstream: ["hash"],
398
+ reason: 'sys.load_buffer example `buffer.get_bytes(my_buffer, "data")`',
399
+ },
400
+ ],
401
+ [
402
+ "gui.set:param:value",
403
+ {
404
+ adds: "Matrix4 | Hash",
405
+ upstream: ["number", "vector4", "vector3", "quaternion"],
406
+ reason:
407
+ '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',
408
+ },
409
+ ],
410
+ [
411
+ "gui.set_rotation:param:rotation",
412
+ {
413
+ adds: "Vector3",
414
+ upstream: ["quaternion", "vector4"],
415
+ reason: "gui.set example `gui.set_rotation(node, vmath.vector3(0,0,45))`",
416
+ },
417
+ ],
418
+ [
419
+ "sys.save:param:table",
420
+ {
421
+ adds: "readonly unknown[]",
422
+ upstream: ["table"],
423
+ reason:
424
+ 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.save`',
425
+ },
426
+ ],
427
+ [
428
+ "sys.serialize:param:table",
429
+ {
430
+ adds: "readonly unknown[]",
431
+ upstream: ["table"],
432
+ reason:
433
+ 'example builds `my_table` with `table.insert(my_table, "my_value")` and passes it to `sys.serialize`',
434
+ },
435
+ ],
436
+ ]);
437
+
438
+ // Parameter doc text whose upstream wording describes Lua rather than the
439
+ // TypeScript a user writes. The transpiler adds one to a literal `options.index`
440
+ // at these calls, so the 1-based wording would tell the author to be off by one.
441
+ // The `.d.ts` emit and the API reference both apply it, and applying it throws
442
+ // when `from` is absent: an upstream rewording fails the build instead of
443
+ // shipping the stale text alongside the lowering.
444
+ export interface ParamDocRewrite {
445
+ readonly from: string;
446
+ readonly to: string;
447
+ }
448
+
449
+ const ZERO_BASED_INDEX_OPTION_DOC: ParamDocRewrite = {
450
+ from: "index into array property (1 based)",
451
+ to: "zero-based index into array property; the transpiler emits Defold's 1-based index",
452
+ };
453
+
454
+ // Keyed `<element>:param:<slot>` like PARAM_TYPE_CORRECTIONS.
455
+ export const PARAM_DOC_REWRITES: ReadonlyMap<string, ParamDocRewrite> = new Map([
456
+ ["go.get:param:options", ZERO_BASED_INDEX_OPTION_DOC],
457
+ ["go.set:param:options", ZERO_BASED_INDEX_OPTION_DOC],
458
+ ["gui.get:param:options", ZERO_BASED_INDEX_OPTION_DOC],
459
+ ["gui.set:param:options", ZERO_BASED_INDEX_OPTION_DOC],
460
+ ]);
461
+
462
+ export function rewriteParamDoc(elementName: string, slotName: string, doc: string): string {
463
+ const key = tableSlotKey(elementName, "param", slotName);
464
+ const rewrite = PARAM_DOC_REWRITES.get(key);
465
+ if (rewrite === undefined) return doc;
466
+ if (!doc.includes(rewrite.from)) {
467
+ throw new Error(`PARAM_DOC_REWRITES ${key}: upstream doc no longer contains "${rewrite.from}"`);
468
+ }
469
+ return doc.replaceAll(rewrite.from, rewrite.to);
470
+ }
471
+
274
472
  // A property whose upstream `<span class="type">` states a type the engine does
275
473
  // not use. Unlike every other override here, which fills a gap upstream left
276
474
  // empty, these contradict a token upstream explicitly declares — so each entry
@@ -319,6 +517,26 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
319
517
  ],
320
518
  ]);
321
519
 
520
+ // A property upstream names with a placeholder standing for a numbered family of
521
+ // keys, so its literal name is not a key the engine accepts. The emitted members
522
+ // are one literal key per number in `first..last`, each typed like the
523
+ // placeholder. `evidence` is the brief phrase that defines the range;
524
+ // `property-correction-provenance.test.ts` reds when a vendored ref-doc stops
525
+ // saying it, or when the phrase stops stating `first-last`. Curated rather than
526
+ // matched on a trailing `N`, so each new upstream placeholder is a deliberate
527
+ // addition.
528
+ export interface PropertyKeyRange {
529
+ readonly prefix: string;
530
+ readonly first: number;
531
+ readonly last: number;
532
+ readonly evidence: string;
533
+ }
534
+
535
+ // Keyed `<namespace>.<property>`, mirroring PROPERTY_TYPE_CORRECTIONS.
536
+ export const PROPERTY_KEY_RANGES: ReadonlyMap<string, PropertyKeyRange> = new Map([
537
+ ["model.textureN", { prefix: "texture", first: 0, last: 7, evidence: "textureN where N is 0-7" }],
538
+ ]);
539
+
322
540
  // A parameter the ref-doc metadata marks required while the same element's prose
323
541
  // or examples show it being omitted. `is_optional` is the only optionality
324
542
  // signal the emitter has, and upstream leaves it `False` on slots its own
@@ -356,11 +574,16 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
356
574
  "b2d.fixture.set_shape:param:update_mass",
357
575
  '2 of 3 examples omit it; "The body mass is not updated unless update_mass is true"',
358
576
  ],
577
+ [
578
+ "b2d.shape.set_shape:param:update_mass",
579
+ '1 of 3 examples omits it; "The body mass is not updated unless update_mass is true"',
580
+ ],
359
581
  [
360
582
  "b2d.shape.ray_cast:param:max_fraction",
361
583
  '"optional maximum translation fraction, defaults to 1"',
362
584
  ],
363
585
  ["iap.buy:param:options", '"optional parameters as properties"'],
586
+ ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
364
587
  ...(
365
588
  [
366
589
  "create_distance",
@@ -382,6 +605,69 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
382
605
  ),
383
606
  ]);
384
607
 
608
+ // A first slot upstream documents as standing in for a second argument list:
609
+ // every `b2d.shape` function takes a shape handle "or pass `body, shape_index`".
610
+ // The element keeps its declared signature and gains an overload with
611
+ // `parameters` in that slot's place. Keyed `<namespace>:param:<slot>`, so only a
612
+ // curated entry adds an overload: a reworded doc cannot add or drop one, and the
613
+ // same prose in another namespace adds nothing.
614
+ //
615
+ // `evidence` is the upstream slot-doc fragment that names the alternative. The
616
+ // emitter throws once a curated slot stops carrying it, so upstream dropping the
617
+ // alternative reds here rather than shipping an overload nothing documents.
618
+ export interface FirstSlotAlternative {
619
+ readonly parameters: readonly ApiParameter[];
620
+ readonly evidence: string;
621
+ }
622
+
623
+ export const FIRST_SLOT_ALTERNATIVES: ReadonlyMap<string, FirstSlotAlternative> = new Map([
624
+ [
625
+ "b2d.shape:param:shape_id",
626
+ {
627
+ parameters: [
628
+ {
629
+ name: "body",
630
+ doc: "The body that owns the shape.",
631
+ types: ["b2Body"],
632
+ isOptional: false,
633
+ },
634
+ {
635
+ name: "shape_index",
636
+ doc: "The shape's 1-based index on that body, as <code>b2d.body.get_shapes</code> lists them.",
637
+ types: ["number"],
638
+ isOptional: false,
639
+ },
640
+ ],
641
+ evidence: "or pass <code>body, shape_index</code>",
642
+ },
643
+ ],
644
+ ]);
645
+
646
+ // The alternative signature FIRST_SLOT_ALTERNATIVES gives `fn`, or null when
647
+ // its first slot is not curated. Shared with the fidelity audit so the arity it
648
+ // measures is the arity the emitter declares.
649
+ export function firstSlotAlternativeOf(fn: ApiFunction, namespace: string): ApiFunction | null {
650
+ const [first, ...rest] = fn.parameters;
651
+ if (first === undefined) return null;
652
+ const key = `${namespace}:param:${first.name}`;
653
+ const alternative = FIRST_SLOT_ALTERNATIVES.get(key);
654
+ if (alternative === undefined) return null;
655
+ if (!first.doc.includes(alternative.evidence)) {
656
+ throw new Error(
657
+ `first-slot alternative ${key}: ${fn.name} no longer documents "${alternative.evidence}"`,
658
+ );
659
+ }
660
+ return { ...fn, parameters: [...alternative.parameters, ...rest] };
661
+ }
662
+
663
+ function firstSlotAlternative(
664
+ prepared: PreparedFunction,
665
+ namespace: string,
666
+ ): PreparedFunction | null {
667
+ const original = firstSlotAlternativeOf(prepared.original, namespace);
668
+ return original === null ? null : { ...prepared, original };
669
+ }
670
+
385
671
  export type ConstantSlotResolution =
386
672
  | { readonly borrow: string }
387
673
  | { readonly family: string; readonly evidence: string }
@@ -415,6 +701,377 @@ export const CONSTANT_SLOT_RESOLUTIONS: ReadonlyMap<string, ConstantSlotResoluti
415
701
  ],
416
702
  ]);
417
703
 
704
+ export interface ConstantUnionAlias {
705
+ // The namespace the alias is declared in. It is the namespace the union
706
+ // *means*, which is not always the one its constants live in: the three
707
+ // buffer keys `render.clear` accepts are `graphics.BUFFER_TYPE_*` constants,
708
+ // but the name that reads right at the call site is `render.ClearBufferKey`.
709
+ readonly home: string;
710
+ readonly name: string;
711
+ // In documentation order, which is the order the arms are emitted in today.
712
+ readonly members: readonly string[];
713
+ }
714
+
715
+ // A documented constant union gets a public name so a caller can spell the type
716
+ // their own wrapper takes. Identity is the *member set*, never a name prefix:
717
+ // `gui.ANCHOR_*` splits into two rows because the x and y slots accept
718
+ // different thirds of it, and `graphics.BUFFER_TYPE_*` splits because
719
+ // `render.clear` accepts three of the six.
720
+ //
721
+ // The table is the only thing the emitter reads, and every name in it has been
722
+ // reviewed: a longest-common-prefix rule proposes, a human disposes. A union
723
+ // with no row keeps its inline spelling, which the coverage gate in
724
+ // `packages/types/test/constant-union-alias-coverage.test.ts` turns into a
725
+ // failure naming the row to review.
726
+ export const CONSTANT_UNION_ALIASES: readonly ConstantUnionAlias[] = [
727
+ {
728
+ home: "buffer",
729
+ name: "ValueType",
730
+ members: [
731
+ "buffer.VALUE_TYPE_FLOAT32",
732
+ "buffer.VALUE_TYPE_INT16",
733
+ "buffer.VALUE_TYPE_INT32",
734
+ "buffer.VALUE_TYPE_INT64",
735
+ "buffer.VALUE_TYPE_INT8",
736
+ "buffer.VALUE_TYPE_UINT16",
737
+ "buffer.VALUE_TYPE_UINT32",
738
+ "buffer.VALUE_TYPE_UINT64",
739
+ "buffer.VALUE_TYPE_UINT8",
740
+ ],
741
+ },
742
+ {
743
+ home: "collectionfactory",
744
+ name: "Status",
745
+ members: [
746
+ "collectionfactory.STATUS_UNLOADED",
747
+ "collectionfactory.STATUS_LOADING",
748
+ "collectionfactory.STATUS_LOADED",
749
+ ],
750
+ },
751
+ {
752
+ home: "factory",
753
+ name: "Status",
754
+ members: ["factory.STATUS_UNLOADED", "factory.STATUS_LOADING", "factory.STATUS_LOADED"],
755
+ },
756
+ {
757
+ home: "go",
758
+ name: "Easing",
759
+ members: [
760
+ "go.EASING_INBACK",
761
+ "go.EASING_INBOUNCE",
762
+ "go.EASING_INCIRC",
763
+ "go.EASING_INCUBIC",
764
+ "go.EASING_INELASTIC",
765
+ "go.EASING_INEXPO",
766
+ "go.EASING_INOUTBACK",
767
+ "go.EASING_INOUTBOUNCE",
768
+ "go.EASING_INOUTCIRC",
769
+ "go.EASING_INOUTCUBIC",
770
+ "go.EASING_INOUTELASTIC",
771
+ "go.EASING_INOUTEXPO",
772
+ "go.EASING_INOUTQUAD",
773
+ "go.EASING_INOUTQUART",
774
+ "go.EASING_INOUTQUINT",
775
+ "go.EASING_INOUTSINE",
776
+ "go.EASING_INQUAD",
777
+ "go.EASING_INQUART",
778
+ "go.EASING_INQUINT",
779
+ "go.EASING_INSINE",
780
+ "go.EASING_LINEAR",
781
+ "go.EASING_OUTBACK",
782
+ "go.EASING_OUTBOUNCE",
783
+ "go.EASING_OUTCIRC",
784
+ "go.EASING_OUTCUBIC",
785
+ "go.EASING_OUTELASTIC",
786
+ "go.EASING_OUTEXPO",
787
+ "go.EASING_OUTINBACK",
788
+ "go.EASING_OUTINBOUNCE",
789
+ "go.EASING_OUTINCIRC",
790
+ "go.EASING_OUTINCUBIC",
791
+ "go.EASING_OUTINELASTIC",
792
+ "go.EASING_OUTINEXPO",
793
+ "go.EASING_OUTINQUAD",
794
+ "go.EASING_OUTINQUART",
795
+ "go.EASING_OUTINQUINT",
796
+ "go.EASING_OUTINSINE",
797
+ "go.EASING_OUTQUAD",
798
+ "go.EASING_OUTQUART",
799
+ "go.EASING_OUTQUINT",
800
+ "go.EASING_OUTSINE",
801
+ ],
802
+ },
803
+ {
804
+ home: "go",
805
+ name: "Playback",
806
+ members: [
807
+ "go.PLAYBACK_ONCE_FORWARD",
808
+ "go.PLAYBACK_ONCE_BACKWARD",
809
+ "go.PLAYBACK_ONCE_PINGPONG",
810
+ "go.PLAYBACK_LOOP_FORWARD",
811
+ "go.PLAYBACK_LOOP_BACKWARD",
812
+ "go.PLAYBACK_LOOP_PINGPONG",
813
+ ],
814
+ },
815
+ {
816
+ home: "graphics",
817
+ name: "BufferType",
818
+ members: [
819
+ "graphics.BUFFER_TYPE_COLOR0_BIT",
820
+ "graphics.BUFFER_TYPE_COLOR1_BIT",
821
+ "graphics.BUFFER_TYPE_COLOR2_BIT",
822
+ "graphics.BUFFER_TYPE_COLOR3_BIT",
823
+ "graphics.BUFFER_TYPE_DEPTH_BIT",
824
+ "graphics.BUFFER_TYPE_STENCIL_BIT",
825
+ ],
826
+ },
827
+ {
828
+ home: "graphics",
829
+ name: "State",
830
+ members: [
831
+ "graphics.STATE_DEPTH_TEST",
832
+ "graphics.STATE_STENCIL_TEST",
833
+ "graphics.STATE_BLEND",
834
+ "graphics.STATE_ALPHA_TEST",
835
+ "graphics.STATE_CULL_FACE",
836
+ "graphics.STATE_POLYGON_OFFSET_FILL",
837
+ ],
838
+ },
839
+ {
840
+ home: "gui",
841
+ name: "AdjustMode",
842
+ members: ["gui.ADJUST_FIT", "gui.ADJUST_ZOOM", "gui.ADJUST_STRETCH"],
843
+ },
844
+ {
845
+ home: "gui",
846
+ name: "BlendMode",
847
+ members: [
848
+ "gui.BLEND_ALPHA",
849
+ "gui.BLEND_ADD",
850
+ "gui.BLEND_ADD_ALPHA",
851
+ "gui.BLEND_MULT",
852
+ "gui.BLEND_SCREEN",
853
+ ],
854
+ },
855
+ {
856
+ home: "gui",
857
+ name: "ClippingMode",
858
+ members: ["gui.CLIPPING_MODE_NONE", "gui.CLIPPING_MODE_STENCIL"],
859
+ },
860
+ {
861
+ home: "gui",
862
+ name: "Easing",
863
+ members: [
864
+ "gui.EASING_INBACK",
865
+ "gui.EASING_INBOUNCE",
866
+ "gui.EASING_INCIRC",
867
+ "gui.EASING_INCUBIC",
868
+ "gui.EASING_INELASTIC",
869
+ "gui.EASING_INEXPO",
870
+ "gui.EASING_INOUTBACK",
871
+ "gui.EASING_INOUTBOUNCE",
872
+ "gui.EASING_INOUTCIRC",
873
+ "gui.EASING_INOUTCUBIC",
874
+ "gui.EASING_INOUTELASTIC",
875
+ "gui.EASING_INOUTEXPO",
876
+ "gui.EASING_INOUTQUAD",
877
+ "gui.EASING_INOUTQUART",
878
+ "gui.EASING_INOUTQUINT",
879
+ "gui.EASING_INOUTSINE",
880
+ "gui.EASING_INQUAD",
881
+ "gui.EASING_INQUART",
882
+ "gui.EASING_INQUINT",
883
+ "gui.EASING_INSINE",
884
+ "gui.EASING_LINEAR",
885
+ "gui.EASING_OUTBACK",
886
+ "gui.EASING_OUTBOUNCE",
887
+ "gui.EASING_OUTCIRC",
888
+ "gui.EASING_OUTCUBIC",
889
+ "gui.EASING_OUTELASTIC",
890
+ "gui.EASING_OUTEXPO",
891
+ "gui.EASING_OUTINBACK",
892
+ "gui.EASING_OUTINBOUNCE",
893
+ "gui.EASING_OUTINCIRC",
894
+ "gui.EASING_OUTINCUBIC",
895
+ "gui.EASING_OUTINELASTIC",
896
+ "gui.EASING_OUTINEXPO",
897
+ "gui.EASING_OUTINQUAD",
898
+ "gui.EASING_OUTINQUART",
899
+ "gui.EASING_OUTINQUINT",
900
+ "gui.EASING_OUTINSINE",
901
+ "gui.EASING_OUTQUAD",
902
+ "gui.EASING_OUTQUART",
903
+ "gui.EASING_OUTQUINT",
904
+ "gui.EASING_OUTSINE",
905
+ ],
906
+ },
907
+ {
908
+ home: "gui",
909
+ name: "KeyboardType",
910
+ members: [
911
+ "gui.KEYBOARD_TYPE_DEFAULT",
912
+ "gui.KEYBOARD_TYPE_EMAIL",
913
+ "gui.KEYBOARD_TYPE_NUMBER_PAD",
914
+ "gui.KEYBOARD_TYPE_PASSWORD",
915
+ ],
916
+ },
917
+ {
918
+ home: "gui",
919
+ name: "NodeType",
920
+ members: [
921
+ "gui.TYPE_BOX",
922
+ "gui.TYPE_TEXT",
923
+ "gui.TYPE_PIE",
924
+ "gui.TYPE_PARTICLEFX",
925
+ "gui.TYPE_CUSTOM",
926
+ ],
927
+ },
928
+ {
929
+ home: "gui",
930
+ name: "PieBounds",
931
+ members: ["gui.PIEBOUNDS_RECTANGLE", "gui.PIEBOUNDS_ELLIPSE"],
932
+ },
933
+ {
934
+ home: "gui",
935
+ name: "Pivot",
936
+ members: [
937
+ "gui.PIVOT_CENTER",
938
+ "gui.PIVOT_N",
939
+ "gui.PIVOT_NE",
940
+ "gui.PIVOT_E",
941
+ "gui.PIVOT_SE",
942
+ "gui.PIVOT_S",
943
+ "gui.PIVOT_SW",
944
+ "gui.PIVOT_W",
945
+ "gui.PIVOT_NW",
946
+ ],
947
+ },
948
+ {
949
+ home: "gui",
950
+ name: "Playback",
951
+ members: [
952
+ "gui.PLAYBACK_ONCE_FORWARD",
953
+ "gui.PLAYBACK_ONCE_BACKWARD",
954
+ "gui.PLAYBACK_ONCE_PINGPONG",
955
+ "gui.PLAYBACK_LOOP_FORWARD",
956
+ "gui.PLAYBACK_LOOP_BACKWARD",
957
+ "gui.PLAYBACK_LOOP_PINGPONG",
958
+ ],
959
+ },
960
+ {
961
+ home: "gui",
962
+ name: "Property",
963
+ members: [
964
+ "gui.PROP_POSITION",
965
+ "gui.PROP_ROTATION",
966
+ "gui.PROP_EULER",
967
+ "gui.PROP_SCALE",
968
+ "gui.PROP_COLOR",
969
+ "gui.PROP_OUTLINE",
970
+ "gui.PROP_SHADOW",
971
+ "gui.PROP_SIZE",
972
+ "gui.PROP_FILL_ANGLE",
973
+ "gui.PROP_INNER_RADIUS",
974
+ "gui.PROP_LEADING",
975
+ "gui.PROP_TRACKING",
976
+ "gui.PROP_SLICE9",
977
+ ],
978
+ },
979
+ {
980
+ home: "gui",
981
+ name: "SafeAreaMode",
982
+ members: [
983
+ "gui.SAFE_AREA_NONE",
984
+ "gui.SAFE_AREA_LONG",
985
+ "gui.SAFE_AREA_SHORT",
986
+ "gui.SAFE_AREA_BOTH",
987
+ ],
988
+ },
989
+ {
990
+ home: "gui",
991
+ name: "SizeMode",
992
+ members: ["gui.SIZE_MODE_MANUAL", "gui.SIZE_MODE_AUTO"],
993
+ },
994
+ {
995
+ home: "gui",
996
+ name: "XAnchor",
997
+ members: ["gui.ANCHOR_NONE", "gui.ANCHOR_LEFT", "gui.ANCHOR_RIGHT"],
998
+ },
999
+ {
1000
+ home: "gui",
1001
+ name: "YAnchor",
1002
+ members: ["gui.ANCHOR_NONE", "gui.ANCHOR_TOP", "gui.ANCHOR_BOTTOM"],
1003
+ },
1004
+ {
1005
+ home: "profiler",
1006
+ name: "Mode",
1007
+ members: [
1008
+ "profiler.MODE_RUN",
1009
+ "profiler.MODE_PAUSE",
1010
+ "profiler.MODE_SHOW_PEAK_FRAME",
1011
+ "profiler.MODE_RECORD",
1012
+ ],
1013
+ },
1014
+ {
1015
+ home: "profiler",
1016
+ name: "ViewMode",
1017
+ members: ["profiler.VIEW_MODE_FULL", "profiler.VIEW_MODE_MINIMIZED"],
1018
+ },
1019
+ {
1020
+ home: "render",
1021
+ name: "ClearBufferKey",
1022
+ members: [
1023
+ "graphics.BUFFER_TYPE_COLOR0_BIT",
1024
+ "graphics.BUFFER_TYPE_DEPTH_BIT",
1025
+ "graphics.BUFFER_TYPE_STENCIL_BIT",
1026
+ ],
1027
+ },
1028
+ {
1029
+ home: "sys",
1030
+ name: "NetworkConnectivity",
1031
+ members: [
1032
+ "sys.NETWORK_DISCONNECTED",
1033
+ "sys.NETWORK_CONNECTED_CELLULAR",
1034
+ "sys.NETWORK_CONNECTED",
1035
+ ],
1036
+ },
1037
+ {
1038
+ home: "window",
1039
+ name: "DimModeState",
1040
+ members: ["window.DIMMING_UNKNOWN", "window.DIMMING_ON", "window.DIMMING_OFF"],
1041
+ },
1042
+ {
1043
+ home: "window",
1044
+ name: "DimModeStateSettable",
1045
+ members: ["window.DIMMING_ON", "window.DIMMING_OFF"],
1046
+ },
1047
+ ];
1048
+
1049
+ const CONSTANT_UNION_ALIAS_BY_MEMBERS: ReadonlyMap<string, ConstantUnionAlias> = new Map(
1050
+ CONSTANT_UNION_ALIASES.map((row) => [constantUnionAliasKey(row.members), row] as const),
1051
+ );
1052
+
1053
+ function constantUnionAliasKey(members: readonly string[]): string {
1054
+ return [...new Set(members)].sort().join("\u0000");
1055
+ }
1056
+
1057
+ // The alias naming exactly these constants, or undefined when no row does. A
1058
+ // single constant is never aliased: it already has a name.
1059
+ export function constantUnionAlias(fqns: readonly string[]): ConstantUnionAlias | undefined {
1060
+ if (fqns.length < 2) return undefined;
1061
+ return CONSTANT_UNION_ALIAS_BY_MEMBERS.get(constantUnionAliasKey(fqns));
1062
+ }
1063
+
1064
+ // The alias declarations a module owns, in name order. A row is declared in its
1065
+ // home whether or not that module references it, so a cross-namespace row is
1066
+ // reachable from the module whose signatures spell it.
1067
+ export function constantUnionAliasDeclarations(namespace: string): string[] {
1068
+ return CONSTANT_UNION_ALIASES.filter((row) => row.home === namespace)
1069
+ .sort((a, b) => a.name.localeCompare(b.name))
1070
+ .map(
1071
+ (row) => `type ${row.name} = ${row.members.map((member) => `typeof ${member}`).join(" | ")};`,
1072
+ );
1073
+ }
1074
+
418
1075
  const DOC_CONSTANT_TOKEN =
419
1076
  /(?<![A-Za-z0-9_.])([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*\.[A-Z][A-Z0-9_]*)(\*)?/g;
420
1077
 
@@ -675,6 +1332,27 @@ const PHYSICS_SHAPE_TABLE_FIELDS: readonly TableField[] = [
675
1332
  ];
676
1333
 
677
1334
  export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new Map([
1335
+ // buffer.create's `declaration` is "a table where each entry (table) describes
1336
+ // a stream", and upstream's example passes a list of stream records whose
1337
+ // `type` is a buffer.VALUE_TYPE_* constant.
1338
+ [
1339
+ "buffer.create:param:declaration",
1340
+ {
1341
+ kind: "array-object",
1342
+ fields: [
1343
+ { name: "name", types: ["hash", "string"] },
1344
+ { name: "type", types: ["constant"], tsType: "buffer.ValueType" },
1345
+ { name: "count", types: ["number"] },
1346
+ ],
1347
+ },
1348
+ ],
1349
+ // collectionfactory.create's `properties` is keyed by "game object ids (hash)",
1350
+ // which upstream's example builds with `hash("/go")`. The `string` key keeps
1351
+ // the object-literal form this slot accepted as a bare `table`.
1352
+ [
1353
+ "collectionfactory.create:param:properties",
1354
+ { kind: "mapping", key: "hash | string", value: "table" },
1355
+ ],
678
1356
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
679
1357
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
680
1358
  // `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
@@ -906,8 +1584,8 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
906
1584
  // names as keys to their clear value: vector4 for the color buffer, number for
907
1585
  // depth/stencil. Both sides are union tokens the mapping string branch splits,
908
1586
  // so the key emits the three branded constants and the value `number | Vector4`.
909
- // The key is branded rather than `number`, which is the narrowing users migrate
910
- // to via the authored `render.ClearBufferKey` alias.
1587
+ // The key set is a CONSTANT_UNION_ALIASES row, so the signature carries the
1588
+ // `render.ClearBufferKey` alias rather than the three branded arms.
911
1589
  [
912
1590
  "render.clear:param:buffers",
913
1591
  {
@@ -1380,11 +2058,33 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1380
2058
  // its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
1381
2059
  // path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
1382
2060
  // (receive, narrow) split.
2061
+ //
2062
+ // The other entries type what the slot's own prose lists and nothing more.
2063
+ // `http.request`'s response fields are the `<ul>` under "The response data.
2064
+ // Contains the fields:"; each is conditional ("if not saved on disc", "only if
2065
+ // option report_progress is true") except `status`, and `headers` is the same
2066
+ // string map the request sends. `sprite.play_flipbook`'s message is the
2067
+ // "Information about the completion" list. `collectionproxy.load` documents
2068
+ // only that the callback receives `proxy_loading`, `proxy_ready` or
2069
+ // `proxy_error` messages and lists no fields, so its message is the bare record
2070
+ // `window.set_listener`'s `data` is.
1383
2071
  export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
1384
2072
  [
1385
2073
  "window.set_listener:param:callback",
1386
2074
  "(self: unknown, event: typeof WINDOW_EVENT_FOCUS_LOST | typeof WINDOW_EVENT_FOCUS_GAINED | typeof WINDOW_EVENT_RESIZED | typeof WINDOW_EVENT_ICONFIED | typeof WINDOW_EVENT_DEICONIFIED, data: Record<string | number, unknown>) => void",
1387
2075
  ],
2076
+ [
2077
+ "http.request:param:callback",
2078
+ "(self: unknown, id: Hash, response: { status: number; response?: string; headers?: LuaMap<string, string>; path?: string; error?: string; bytes_received?: number; bytes_total?: number; range_start?: number; range_end?: number; document_size?: number }) => void",
2079
+ ],
2080
+ [
2081
+ "sprite.play_flipbook:param:complete_function",
2082
+ "(self: unknown, message_id: Hash, message: { current_tile: number; id: Hash }, sender: Url) => void",
2083
+ ],
2084
+ [
2085
+ "collectionproxy.load:param:callback",
2086
+ "(self: unknown, message_id: Hash, message: Record<string | number, unknown>, sender: Url) => void",
2087
+ ],
1388
2088
  ]);
1389
2089
 
1390
2090
  // resource.set_atlas's `table` param and resource.get_atlas's `data` return are
@@ -1417,7 +2117,19 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
1417
2117
  // parser recovers every other field faithfully but leaves that `table` token as
1418
2118
  // a bare `Record`; this pins only the `value` field's TS type to the documented
1419
2119
  // array union while every sibling field stays parser-authoritative. Keyed by
1420
- // `<element>:<kind>:<slot>:<field>`, mirroring NESTED_FIELD_CURATIONS.
2120
+ // `<element>:<kind>:<slot>:<field>`, mirroring NESTED_FIELD_CURATIONS; a field
2121
+ // one level down is `<field>.<member>`, or `<field>[].<member>` inside a list.
2122
+ //
2123
+ // The atlas animation `playback` field is documented as `constant` with the
2124
+ // prose "the default value is go.PLAYBACK_ONCE_FORWARD", which names the enum
2125
+ // the bare token cannot.
2126
+ //
2127
+ // `gui.set`'s `options.key` is documented as `hash`, while upstream's own
2128
+ // example sets a named font with `{ key = "my_font_name" }`.
2129
+ //
2130
+ // `render.set_render_target`'s `options.transient` is documented as `table`
2131
+ // with the prose "a list of buffer types", and upstream's example passes
2132
+ // graphics.BUFFER_TYPE_* constants.
1421
2133
  const CONSTANT_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | (Vector4 | Matrix4)[]";
1422
2134
  const ATTRIBUTE_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | number[]";
1423
2135
  export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
@@ -1425,6 +2137,40 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
1425
2137
  ["material.set_constants:param:constants:value", CONSTANT_VALUE_TS],
1426
2138
  ["material.set_vertex_attributes:param:attributes:value", ATTRIBUTE_VALUE_TS],
1427
2139
  ["material.get_vertex_attributes:return:table:value", ATTRIBUTE_VALUE_TS],
2140
+ ["resource.set_atlas:param:table:animations[].playback", "go.Playback"],
2141
+ ["resource.create_atlas:param:table:animations[].playback", "go.Playback"],
2142
+ ["resource.get_atlas:return:data:animations[].playback", "go.Playback"],
2143
+ ["gui.set:param:options:key", "string | Hash"],
2144
+ ["render.set_render_target:param:options:transient", "graphics.BufferType[]"],
2145
+ ]);
2146
+
2147
+ // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
2148
+ // documents in full. Every other authored table here retypes, marks optional or
2149
+ // suppresses a slot the parser already recovered; this one supplies a member
2150
+ // upstream omits, leaving the documented fields parser-authoritative so an
2151
+ // upstream edit to any of them lands without a hand-maintained restatement.
2152
+ //
2153
+ // `evidence` pins where upstream itself shows the field. applyFieldAdditions
2154
+ // throws once the parse recovers it, so upstream documenting the field reds
2155
+ // here and the entry is deleted rather than re-pinned.
2156
+ //
2157
+ // Keyed `<element>:<kind>:<slot>`, the TABLE_SLOT_CURATIONS shape.
2158
+ export interface TableFieldAddition {
2159
+ readonly field: TableField;
2160
+ readonly evidence: readonly string[];
2161
+ }
2162
+
2163
+ export const TABLE_SLOT_FIELD_ADDITIONS: ReadonlyMap<string, TableFieldAddition> = new Map([
2164
+ [
2165
+ "resource.create_texture:param:table",
2166
+ {
2167
+ field: { name: "page_count", types: ["number"], optional: true },
2168
+ evidence: [
2169
+ "the array-texture example on this element creates with `page_count = 5`",
2170
+ "resource.get_texture_info documents `page_count` as the texture array's page count",
2171
+ ],
2172
+ },
2173
+ ],
1428
2174
  ]);
1429
2175
 
1430
2176
  // A return-slot field upstream documents as platform-gated or nil-valued while
@@ -1516,6 +2262,26 @@ export function applyFieldOptionalityCorrections(
1516
2262
  return corrected;
1517
2263
  }
1518
2264
 
2265
+ // Append the member TABLE_SLOT_FIELD_ADDITIONS supplies for this slot, after
2266
+ // every field the parser recovered. Throws on an addition whose field the parse
2267
+ // already produced: upstream has documented it, and the entry is stale.
2268
+ // Returns a new array; never mutates the parser's result.
2269
+ export function applyFieldAdditions(
2270
+ elementName: string,
2271
+ slotKind: "param" | "return" | undefined,
2272
+ slotName: string | undefined,
2273
+ fields: readonly TableField[],
2274
+ ): TableField[] {
2275
+ if (slotKind === undefined || slotName === undefined) return [...fields];
2276
+ const key = tableSlotKey(elementName, slotKind, slotName);
2277
+ const addition = TABLE_SLOT_FIELD_ADDITIONS.get(key);
2278
+ if (addition === undefined) return [...fields];
2279
+ if (fields.some((field) => field.name === addition.field.name)) {
2280
+ throw new Error(`field addition names a recovered field: ${key}:${addition.field.name}`);
2281
+ }
2282
+ return [...fields, addition.field];
2283
+ }
2284
+
1519
2285
  // Pin a parser-recovered field's TS type from TABLE_FIELD_TYPE_OVERRIDES,
1520
2286
  // leaving every other field parser-authoritative. A field the override does not
1521
2287
  // name is returned unchanged; a field it names gains a `tsType` (emitted
@@ -1528,11 +2294,17 @@ export function applyFieldTypeOverrides(
1528
2294
  fields: readonly TableField[],
1529
2295
  ): TableField[] {
1530
2296
  if (slotKind === undefined || slotName === undefined) return [...fields];
1531
- return fields.map((field) => {
1532
- const override = TABLE_FIELD_TYPE_OVERRIDES.get(
1533
- `${elementName}:${slotKind}:${slotName}:${field.name}`,
1534
- );
2297
+ const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
2298
+ const typed = (field: TableField, path: string): TableField => {
2299
+ const override = TABLE_FIELD_TYPE_OVERRIDES.get(`${prefix}${path}`);
1535
2300
  return override === undefined ? field : { ...field, tsType: override };
2301
+ };
2302
+ return fields.map((field) => {
2303
+ const own = typed(field, field.name);
2304
+ if (own.fields === undefined) return own;
2305
+ const parent = `${field.name}${field.isList === true ? "[]" : ""}`;
2306
+ const nested = own.fields.map((child) => typed(child, `${parent}.${child.name}`));
2307
+ return nested.every((child, i) => child === own.fields?.[i]) ? own : { ...own, fields: nested };
1536
2308
  });
1537
2309
  }
1538
2310
 
@@ -1864,6 +2636,15 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
1864
2636
  return grouped;
1865
2637
  }
1866
2638
 
2639
+ // A handle the type map widens (`constant_buffer` takes named constants) keeps
2640
+ // that shape, so annotating with the alias loses nothing the handle-returning
2641
+ // function yields.
2642
+ function typedefAlias(name: string, mapType: (token: string) => string): string {
2643
+ const brand = `Opaque<"${name}">`;
2644
+ const mapped = mapType(name);
2645
+ return `type ${name} = ${mapped.startsWith(brand) ? mapped : brand};`;
2646
+ }
2647
+
1867
2648
  export function emitDeclarations(module: ApiModule, options?: EmitOptions): string {
1868
2649
  const prefix = `${module.namespace}.`;
1869
2650
 
@@ -1946,7 +2727,10 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1946
2727
  // A typedef that also has colon methods is emitted as a method-bearing
1947
2728
  // interface below, not an opaque brand alias.
1948
2729
  if (handleGroups.has(t.name)) continue;
1949
- lines.push(`${INDENT}${decl}type ${t.name} = Opaque<"${t.name}">;`);
2730
+ lines.push(`${INDENT}${decl}${typedefAlias(t.name, baseMapType)}`);
2731
+ }
2732
+ for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
2733
+ lines.push(`${INDENT}${decl}${aliasDecl}`);
1950
2734
  }
1951
2735
  const handleIndent = `${INDENT}${INDENT}`;
1952
2736
  for (const receiver of handleReceivers) {
@@ -1989,6 +2773,18 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
1989
2773
  for (const docLine of functionDocLines(fn.original, translations)) lines.push(docLine);
1990
2774
  const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
1991
2775
  lines.push(`${INDENT}${reserved ? "" : decl}${line}`);
2776
+ const alternative = firstSlotAlternative(fn, module.namespace);
2777
+ if (alternative !== null) {
2778
+ const overload = emitFunction(
2779
+ alternative,
2780
+ emitName,
2781
+ mapType,
2782
+ resolver,
2783
+ constantTokens,
2784
+ urlParameters,
2785
+ );
2786
+ lines.push(`${INDENT}${reserved ? "" : decl}${overload}`);
2787
+ }
1992
2788
  }
1993
2789
 
1994
2790
  for (const alias of [...aliases].sort((a, b) => a.public.localeCompare(b.public))) {
@@ -2051,10 +2847,12 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2051
2847
  const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
2052
2848
  lines.push(`${INDENT}${decl}interface properties {`);
2053
2849
  for (const p of members) {
2054
- for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
2055
- lines.push(docLine);
2850
+ for (const member of emitPropertyMembers(p, mapType, module.namespace)) {
2851
+ for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
2852
+ lines.push(docLine);
2853
+ }
2854
+ lines.push(`${INDENT}${INDENT}${member}`);
2056
2855
  }
2057
- lines.push(`${INDENT}${INDENT}${emitPropertyMember(p, mapType, module.namespace)}`);
2058
2856
  }
2059
2857
  lines.push(`${INDENT}}`);
2060
2858
  }
@@ -2094,7 +2892,10 @@ export interface SymbolSignature {
2094
2892
  * {@link collectNestedGroups} tree the declaration emitter walks, so neither
2095
2893
  * surface can decide on its own which members are nested or how deep they go.
2096
2894
  * The caller applies the same `skipFunctions` filter the `.d.ts` generation
2097
- * does, so a dropped member never yields a signature.
2895
+ * does, so a dropped member never yields a signature. A top-level function the
2896
+ * declaration emits with a first-slot alternative overload carries one arm per
2897
+ * line, primary first, the same convention as the authored fold; its
2898
+ * `slotTypes` describe the primary arm only.
2098
2899
  */
2099
2900
  export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions): SymbolSignature[] {
2100
2901
  const prefix = `${module.namespace}.`;
@@ -2134,19 +2935,28 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2134
2935
  const prepared = prepareFunction(fn, prefix);
2135
2936
  if (prepared === null) continue;
2136
2937
  const slotTypes: Record<string, string> = {};
2137
- out.push({
2138
- identity: fnIdentity(fn),
2139
- tsSignature: emitFunction(
2140
- prepared,
2141
- emitName(prepared.name),
2142
- mapType,
2143
- resolver,
2144
- constantTokens,
2145
- urlParameters,
2146
- slotTypes,
2147
- ),
2938
+ const primary = emitFunction(
2939
+ prepared,
2940
+ emitName(prepared.name),
2941
+ mapType,
2942
+ resolver,
2943
+ constantTokens,
2944
+ urlParameters,
2148
2945
  slotTypes,
2149
- });
2946
+ );
2947
+ const alternative = firstSlotAlternative(prepared, module.namespace);
2948
+ const tsSignature =
2949
+ alternative === null
2950
+ ? primary
2951
+ : `${primary}\n${emitFunction(
2952
+ alternative,
2953
+ emitName(prepared.name),
2954
+ mapType,
2955
+ resolver,
2956
+ constantTokens,
2957
+ urlParameters,
2958
+ )}`;
2959
+ out.push({ identity: fnIdentity(fn), tsSignature, slotTypes });
2150
2960
  }
2151
2961
 
2152
2962
  const nested = flattenNestedGroups(collectNestedGroups(module, prefix));
@@ -2218,10 +3028,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2218
3028
  });
2219
3029
  }
2220
3030
 
3031
+ // A key-range property emits several members that share one type; its entry
3032
+ // carries the first, which is verbatim in the `.d.ts` and is what the `/api`
3033
+ // render layer reduces to `<placeholder>: <type>`.
2221
3034
  for (const p of module.properties) {
3035
+ const [firstMember = ""] = emitPropertyMembers(p, mapType, module.namespace);
2222
3036
  out.push({
2223
3037
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
2224
- tsSignature: emitPropertyMember(p, mapType, module.namespace),
3038
+ tsSignature: firstMember,
2225
3039
  slotTypes: NO_SLOTS,
2226
3040
  });
2227
3041
  }
@@ -2230,7 +3044,20 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2230
3044
  if (!TS_IDENTIFIER.test(t.name) || handleGroups.has(t.name)) continue;
2231
3045
  out.push({
2232
3046
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
2233
- tsSignature: `type ${t.name} = Opaque<"${t.name}">;`,
3047
+ tsSignature: typedefAlias(t.name, baseMapType),
3048
+ slotTypes: NO_SLOTS,
3049
+ });
3050
+ }
3051
+
3052
+ // The module's constant-union aliases travel as typedefs too: a signature that
3053
+ // names one carries no member list of its own, so the docs read the members
3054
+ // from here rather than from the emitter's table.
3055
+ for (const row of CONSTANT_UNION_ALIASES.filter((r) => r.home === module.namespace)) {
3056
+ out.push({
3057
+ identity: { namespace: module.namespace, kind: "TYPEDEF", name: row.name, signature: "" },
3058
+ tsSignature: `type ${row.name} = ${row.members
3059
+ .map((member) => `typeof ${member}`)
3060
+ .join(" | ")};`,
2234
3061
  slotTypes: NO_SLOTS,
2235
3062
  });
2236
3063
  }
@@ -2534,15 +3361,39 @@ function functionDocLines(
2534
3361
  ): string[] {
2535
3362
  const params = fn.parameters.map((p, index) => ({
2536
3363
  name: emittedParamName(p, index),
2537
- doc: htmlToDocText(p.doc),
3364
+ doc: rewriteParamDoc(fn.name, p.name, htmlToDocText(p.doc)),
2538
3365
  }));
2539
3366
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
2540
3367
  const lua = htmlToCodeText(fn.examples ?? "");
2541
- // A hand-authored TS translation pinned to this exact Lua flips the fence to
2542
- // ```ts; any hash mismatch (or absent translation) keeps the Lua fallback.
3368
+ // A blob carrying several examples documents them as several, but only when
3369
+ // every segment has an authored body: a partial resolve would drop the rest.
3370
+ // Otherwise the whole-blob ladder stands — a hand-authored TS translation
3371
+ // pinned to this exact Lua flips the fence to ```ts; any hash mismatch (or
3372
+ // absent translation) keeps the Lua fallback.
3373
+ const segments = splitExampleSources(fn.examples ?? "");
3374
+ const perSegment =
3375
+ segments.length > 1
3376
+ ? lookupExampleTranslations(
3377
+ translations,
3378
+ fn.name,
3379
+ segments.map((segment) => hashExampleSource(segment.code)),
3380
+ )
3381
+ : null;
2543
3382
  const ts = lua === "" ? null : lookupTranslation(translations, fn.name, hashExampleSource(lua));
2544
- const exampleParts: Pick<DocCommentParts, "example" | "exampleLang"> =
2545
- ts !== null ? { example: ts, exampleLang: "ts" } : lua !== "" ? { example: lua } : {};
3383
+ const exampleParts: Pick<DocCommentParts, "examples"> =
3384
+ perSegment !== null
3385
+ ? {
3386
+ examples: perSegment.map((text, index) => ({
3387
+ text,
3388
+ lang: "ts" as const,
3389
+ ...(segments[index]?.prose ? { prose: segments[index]?.prose } : {}),
3390
+ })),
3391
+ }
3392
+ : ts !== null
3393
+ ? { examples: [{ text: ts, lang: "ts" }] }
3394
+ : lua !== ""
3395
+ ? { examples: [{ text: lua, lang: "lua" }] }
3396
+ : {};
2546
3397
  const parts: DocCommentParts = {
2547
3398
  summary: htmlToDocText(summaryFor(fn.brief, fn.description)),
2548
3399
  params,
@@ -2641,19 +3492,22 @@ function parameterType(
2641
3492
  // `safeParamName` fallback.
2642
3493
  const alias = SCENE_ADDRESS_ALIASES[classifyUrlParameter(urlParameters, elementName, p.name)];
2643
3494
  const slotMapType = alias === undefined ? mapType : addressMapType(mapType, alias);
2644
- return concrete.length > 0
2645
- ? mapSlotUnion(
2646
- concrete,
2647
- p.doc,
2648
- slotMapType,
2649
- true,
2650
- resolver,
2651
- constantTokens,
2652
- elementName,
2653
- "param",
2654
- p.name,
2655
- )
2656
- : "unknown";
3495
+ const mapped =
3496
+ concrete.length > 0
3497
+ ? mapSlotUnion(
3498
+ concrete,
3499
+ p.doc,
3500
+ slotMapType,
3501
+ true,
3502
+ resolver,
3503
+ constantTokens,
3504
+ elementName,
3505
+ "param",
3506
+ p.name,
3507
+ )
3508
+ : "unknown";
3509
+ const correction = PARAM_TYPE_CORRECTIONS.get(tableSlotKey(elementName, "param", p.name));
3510
+ return correction === undefined ? mapped : `${mapped} | ${correction.adds}`;
2657
3511
  }
2658
3512
 
2659
3513
  const VARARG_PREFIX = "...";
@@ -2802,12 +3656,19 @@ function emitReturn(
2802
3656
  return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
2803
3657
  }
2804
3658
 
2805
- function emitPropertyMember(
3659
+ function emitPropertyMembers(
2806
3660
  p: ApiProperty,
2807
3661
  mapType: (t: string) => string,
2808
3662
  namespace: string,
2809
- ): string {
2810
- const key = TS_IDENTIFIER.test(p.name) ? p.name : JSON.stringify(p.name);
3663
+ ): readonly string[] {
3664
+ const keyRange = PROPERTY_KEY_RANGES.get(`${namespace}.${p.name}`);
3665
+ const keys: string[] = [];
3666
+ if (keyRange !== undefined) {
3667
+ for (let unit = keyRange.first; unit <= keyRange.last; unit++)
3668
+ keys.push(`${keyRange.prefix}${unit}`);
3669
+ } else {
3670
+ keys.push(TS_IDENTIFIER.test(p.name) ? p.name : JSON.stringify(p.name));
3671
+ }
2811
3672
  const correction = PROPERTY_TYPE_CORRECTIONS.get(`${namespace}.${p.name}`);
2812
3673
  const ts =
2813
3674
  correction !== undefined
@@ -2815,7 +3676,7 @@ function emitPropertyMember(
2815
3676
  : p.types.length > 0
2816
3677
  ? unionFromTokens(p.types, mapType)
2817
3678
  : "unknown";
2818
- return `${key}: ${ts};`;
3679
+ return keys.map((key) => `${key}: ${ts};`);
2819
3680
  }
2820
3681
 
2821
3682
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
@@ -2880,11 +3741,21 @@ function mapSlotUnion(
2880
3741
  // A mapping key may be a `A | B` union token too (render.clear's three
2881
3742
  // documented graphics.BUFFER_TYPE_* constants); split it the same way
2882
3743
  // the value above is split. A single-token key (no `|`) is unaffected.
2883
- const key = unionFromTokens(
2884
- mapping.key.split("|").map((token) => token.trim()),
2885
- mapType,
2886
- );
2887
- ts = `LuaMap<${key}, ${value}>`;
3744
+ const keyTokens = mapping.key.split("|").map((token) => token.trim());
3745
+ if (slotKind === "param" && keyTokens.includes("string")) {
3746
+ // A caller builds a string-keyed parameter table as an object literal,
3747
+ // which `LuaMap` rejects; the `Record` arm admits it and TSTL lowers
3748
+ // both to the same Lua table. The `Record` arm carries the string keys,
3749
+ // so `LuaMap` keeps only the other key tokens: `LuaMap`'s key is
3750
+ // invariant, and a `LuaMap<Hash | string, V>` would reject the
3751
+ // `LuaMap<Hash, V>` a caller builds. A string-only key keeps its
3752
+ // `LuaMap` arm so an existing `LuaMap` caller stays valid.
3753
+ const otherKeys = keyTokens.filter((token) => token !== "string");
3754
+ const key = unionFromTokens(otherKeys.length > 0 ? otherKeys : keyTokens, mapType);
3755
+ ts = `LuaMap<${key}, ${value}> | Record<string, ${value}>`;
3756
+ } else {
3757
+ ts = `LuaMap<${unionFromTokens(keyTokens, mapType)}, ${value}>`;
3758
+ }
2888
3759
  } else if (element !== undefined) {
2889
3760
  ts = arrayTypeFromTokens(element, mapType);
2890
3761
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {
@@ -2911,7 +3782,8 @@ function mapSlotUnion(
2911
3782
  } else {
2912
3783
  const parsed = parseTableFields(doc, resolver, slotName);
2913
3784
  if (parsed !== null) {
2914
- const nested = applyNestedFieldCurations(elementName, slotKind, slotName, parsed);
3785
+ const added = applyFieldAdditions(elementName, slotKind, slotName, parsed);
3786
+ const nested = applyNestedFieldCurations(elementName, slotKind, slotName, added);
2915
3787
  const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
2916
3788
  const fields = applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed);
2917
3789
  const object = inlineTableType(fields, mapType, optionalFields);
@@ -2940,7 +3812,7 @@ function mapSlotUnion(
2940
3812
  // every other union member, so collapse to exactly `unknown` rather than emit
2941
3813
  // a redundant `T | unknown` or `unknown | undefined`.
2942
3814
  if (mapped.includes("unknown")) return "unknown";
2943
- return mapped.join(" | ");
3815
+ return joinUnionArms(mapped);
2944
3816
  }
2945
3817
 
2946
3818
  function tableSlotKey(elementName: string, slotKind: "param" | "return", slotName: string): string {
@@ -3030,7 +3902,36 @@ function unionFromTokens(tokens: readonly string[], mapType: (t: string) => stri
3030
3902
  seen.add(ts);
3031
3903
  mapped.push(ts);
3032
3904
  }
3033
- return mapped.join(" | ");
3905
+ return joinUnionArms(mapped);
3906
+ }
3907
+
3908
+ const BRAND_ARM = /^number & \{ readonly __brand: "([^"]+)" \}$/;
3909
+
3910
+ // Joins already-mapped arms, collapsing the branded ones to their alias when
3911
+ // they are exactly a `CONSTANT_UNION_ALIASES` member set. The alias takes the
3912
+ // position of the first branded arm, so a mixed union keeps its reading order
3913
+ // (`gui.Easing | Vector`). Arms that are not constants are untouched, and a set
3914
+ // with no row keeps every arm inline.
3915
+ function joinUnionArms(mapped: readonly string[]): string {
3916
+ const branded: string[] = [];
3917
+ for (const arm of mapped) {
3918
+ const match = BRAND_ARM.exec(arm);
3919
+ if (match !== null) branded.push(match[1] as string);
3920
+ }
3921
+ const alias = constantUnionAlias(branded);
3922
+ if (alias === undefined) return mapped.join(" | ");
3923
+ const out: string[] = [];
3924
+ let placed = false;
3925
+ for (const arm of mapped) {
3926
+ if (BRAND_ARM.test(arm)) {
3927
+ if (placed) continue;
3928
+ out.push(`${alias.home}.${alias.name}`);
3929
+ placed = true;
3930
+ continue;
3931
+ }
3932
+ out.push(arm);
3933
+ }
3934
+ return out.join(" | ");
3034
3935
  }
3035
3936
 
3036
3937
  export function isKnownDefoldTypeToken(token: string): boolean {