@defold-typescript/types 0.39.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 (87) hide show
  1. package/api-availability.json +117 -0
  2. package/api-signatures.json +262 -237
  3. package/examples/translations.json +81 -81
  4. package/generated/b2d_shape.d.ts +23 -1
  5. package/generated/buffer.d.ts +18 -15
  6. package/generated/camera.d.ts +2 -2
  7. package/generated/collectionfactory.d.ts +12 -10
  8. package/generated/collectionproxy.d.ts +1 -1
  9. package/generated/compute.d.ts +1 -1
  10. package/generated/go.d.ts +9 -5
  11. package/generated/graphics.d.ts +52 -0
  12. package/generated/gui.d.ts +53 -32
  13. package/generated/html5.d.ts +3 -1
  14. package/generated/http.d.ts +4 -2
  15. package/generated/image.d.ts +24 -15
  16. package/generated/kinds/gui-script.d.ts +1 -0
  17. package/generated/kinds/render-script.d.ts +1 -0
  18. package/generated/kinds/script.d.ts +1 -0
  19. package/generated/material.d.ts +1 -1
  20. package/generated/model.d.ts +32 -3
  21. package/generated/physics.d.ts +13 -11
  22. package/generated/profiler.d.ts +1 -1
  23. package/generated/render.d.ts +72 -15
  24. package/generated/resource.d.ts +100 -69
  25. package/generated/socket.d.ts +23 -13
  26. package/generated/sound.d.ts +3 -3
  27. package/generated/sprite.d.ts +9 -1
  28. package/generated/sys.d.ts +34 -23
  29. package/generated/versions/defold-1.12.4/buffer.d.ts +18 -15
  30. package/generated/versions/defold-1.12.4/camera.d.ts +2 -2
  31. package/generated/versions/defold-1.12.4/collectionfactory.d.ts +12 -10
  32. package/generated/versions/defold-1.12.4/go.d.ts +9 -5
  33. package/generated/versions/defold-1.12.4/gui.d.ts +47 -31
  34. package/generated/versions/defold-1.12.4/html5.d.ts +3 -1
  35. package/generated/versions/defold-1.12.4/http.d.ts +4 -2
  36. package/generated/versions/defold-1.12.4/image.d.ts +24 -15
  37. package/generated/versions/defold-1.12.4/index.d.ts +1 -0
  38. package/generated/versions/defold-1.12.4/model.d.ts +32 -3
  39. package/generated/versions/defold-1.12.4/physics.d.ts +13 -11
  40. package/generated/versions/defold-1.12.4/profiler.d.ts +1 -1
  41. package/generated/versions/defold-1.12.4/render.d.ts +72 -15
  42. package/generated/versions/defold-1.12.4/resource.d.ts +100 -69
  43. package/generated/versions/defold-1.12.4/socket.d.ts +23 -13
  44. package/generated/versions/defold-1.12.4/sound.d.ts +3 -3
  45. package/generated/versions/defold-1.12.4/sprite.d.ts +9 -1
  46. package/generated/versions/defold-1.12.4/sys.d.ts +34 -23
  47. package/generated/versions/defold-1.12.4/vmath.d.ts +5 -5
  48. package/generated/versions/defold-1.13.0/b2d_shape.d.ts +23 -1
  49. package/generated/versions/defold-1.13.0/buffer.d.ts +18 -15
  50. package/generated/versions/defold-1.13.0/camera.d.ts +2 -2
  51. package/generated/versions/defold-1.13.0/collectionfactory.d.ts +12 -10
  52. package/generated/versions/defold-1.13.0/compute.d.ts +1 -1
  53. package/generated/versions/defold-1.13.0/go.d.ts +9 -5
  54. package/generated/versions/defold-1.13.0/graphics.d.ts +48 -0
  55. package/generated/versions/defold-1.13.0/gui.d.ts +47 -31
  56. package/generated/versions/defold-1.13.0/html5.d.ts +3 -1
  57. package/generated/versions/defold-1.13.0/http.d.ts +4 -2
  58. package/generated/versions/defold-1.13.0/image.d.ts +24 -15
  59. package/generated/versions/defold-1.13.0/index.d.ts +1 -0
  60. package/generated/versions/defold-1.13.0/material.d.ts +1 -1
  61. package/generated/versions/defold-1.13.0/model.d.ts +32 -3
  62. package/generated/versions/defold-1.13.0/physics.d.ts +13 -11
  63. package/generated/versions/defold-1.13.0/profiler.d.ts +1 -1
  64. package/generated/versions/defold-1.13.0/render.d.ts +72 -15
  65. package/generated/versions/defold-1.13.0/resource.d.ts +100 -69
  66. package/generated/versions/defold-1.13.0/socket.d.ts +23 -13
  67. package/generated/versions/defold-1.13.0/sound.d.ts +3 -3
  68. package/generated/versions/defold-1.13.0/sprite.d.ts +9 -1
  69. package/generated/versions/defold-1.13.0/sys.d.ts +34 -23
  70. package/generated/versions/defold-1.13.0/vmath.d.ts +5 -5
  71. package/generated/vmath.d.ts +5 -5
  72. package/index.d.ts +1 -0
  73. package/package.json +1 -1
  74. package/scripts/generate-api-signatures.ts +2 -1
  75. package/scripts/prose-constants.ts +96 -0
  76. package/scripts/regen.ts +6 -2
  77. package/src/component-properties.d.ts +33 -0
  78. package/src/core-types.ts +4 -2
  79. package/src/custom-messages.d.ts +4 -3
  80. package/src/doc-comment.ts +35 -12
  81. package/src/emit-dts.ts +457 -47
  82. package/src/engine-globals.d.ts +2 -2
  83. package/src/go-overloads.d.ts +11 -10
  84. package/src/index.ts +4 -0
  85. package/src/param-correction-provenance.ts +67 -0
  86. package/src/render-overloads.d.ts +15 -0
  87. package/src/script-api.ts +3 -1
package/src/emit-dts.ts CHANGED
@@ -131,6 +131,8 @@ export const ARBITRARY_TABLE_SLOTS = new Set([
131
131
  // names; bare `on_message`'s receive `message` is the pre-hashed, user-routed
132
132
  // payload (mitigated for authored code by isMessage/onMessage). No static shape
133
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`.
134
136
  "factory.create",
135
137
  "collectionfactory.create",
136
138
  "on_message",
@@ -254,6 +256,16 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
254
256
  // ref-doc. Upstream adding `nil` to `types` hands the case to the top-level
255
257
  // `nil` projection in `mapSlotUnion` and reds the entry, which is then deleted
256
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.
257
269
  export interface ReturnTypeCorrection {
258
270
  readonly ts: string;
259
271
  readonly upstream: readonly string[];
@@ -270,8 +282,193 @@ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection>
270
282
  reason: "the body if successful. Otherwise nil.",
271
283
  },
272
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
+ ],
301
+ ]);
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],
273
460
  ]);
274
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
+
275
472
  // A property whose upstream `<span class="type">` states a type the engine does
276
473
  // not use. Unlike every other override here, which fills a gap upstream left
277
474
  // empty, these contradict a token upstream explicitly declares — so each entry
@@ -320,6 +517,26 @@ export const PROPERTY_TYPE_CORRECTIONS: ReadonlyMap<string, PropertyTypeCorrecti
320
517
  ],
321
518
  ]);
322
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
+
323
540
  // A parameter the ref-doc metadata marks required while the same element's prose
324
541
  // or examples show it being omitted. `is_optional` is the only optionality
325
542
  // signal the emitter has, and upstream leaves it `False` on slots its own
@@ -357,11 +574,16 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
357
574
  "b2d.fixture.set_shape:param:update_mass",
358
575
  '2 of 3 examples omit it; "The body mass is not updated unless update_mass is true"',
359
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
+ ],
360
581
  [
361
582
  "b2d.shape.ray_cast:param:max_fraction",
362
583
  '"optional maximum translation fraction, defaults to 1"',
363
584
  ],
364
585
  ["iap.buy:param:options", '"optional parameters as properties"'],
586
+ ["socket.newtry:param:finalizer", "socket.protect's example `local try = socket.newtry()`"],
365
587
  ...(
366
588
  [
367
589
  "create_distance",
@@ -383,6 +605,69 @@ export const OPTIONAL_SLOT_CORRECTIONS: ReadonlyMap<string, string> = new Map([
383
605
  ),
384
606
  ]);
385
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
+
386
671
  export type ConstantSlotResolution =
387
672
  | { readonly borrow: string }
388
673
  | { readonly family: string; readonly evidence: string }
@@ -1047,6 +1332,27 @@ const PHYSICS_SHAPE_TABLE_FIELDS: readonly TableField[] = [
1047
1332
  ];
1048
1333
 
1049
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
+ ],
1050
1356
  ["collectionfactory.create:return:ids", { kind: "mapping", key: "hash", value: "hash" }],
1051
1357
  // font.get_info's `info` return is a `<dl>` with `path: hash` and a nested
1052
1358
  // `fonts` field whose `<dd>` is itself a `<dl>` (`path: string`, `path_hash:
@@ -1752,11 +2058,33 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
1752
2058
  // its `data` is a bare record (only resize carries fields — `isWindowEvent` is the
1753
2059
  // path to typed `data`), mirroring the `msg.post` (send, typed) / `isMessage`
1754
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.
1755
2071
  export const CALLBACK_SIGNATURE_CURATIONS: ReadonlyMap<string, string> = new Map([
1756
2072
  [
1757
2073
  "window.set_listener:param:callback",
1758
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",
1759
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
+ ],
1760
2088
  ]);
1761
2089
 
1762
2090
  // resource.set_atlas's `table` param and resource.get_atlas's `data` return are
@@ -1789,7 +2117,19 @@ export const NESTED_FIELD_CURATIONS: ReadonlyMap<string, readonly TableField[]>
1789
2117
  // parser recovers every other field faithfully but leaves that `table` token as
1790
2118
  // a bare `Record`; this pins only the `value` field's TS type to the documented
1791
2119
  // array union while every sibling field stays parser-authoritative. Keyed by
1792
- // `<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.
1793
2133
  const CONSTANT_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | (Vector4 | Matrix4)[]";
1794
2134
  const ATTRIBUTE_VALUE_TS = "Vector4 | Vector3 | Matrix4 | number | number[]";
1795
2135
  export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
@@ -1797,6 +2137,11 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
1797
2137
  ["material.set_constants:param:constants:value", CONSTANT_VALUE_TS],
1798
2138
  ["material.set_vertex_attributes:param:attributes:value", ATTRIBUTE_VALUE_TS],
1799
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[]"],
1800
2145
  ]);
1801
2146
 
1802
2147
  // A field the engine accepts on an option bag whose `<dl>` upstream otherwise
@@ -1949,11 +2294,17 @@ export function applyFieldTypeOverrides(
1949
2294
  fields: readonly TableField[],
1950
2295
  ): TableField[] {
1951
2296
  if (slotKind === undefined || slotName === undefined) return [...fields];
1952
- return fields.map((field) => {
1953
- const override = TABLE_FIELD_TYPE_OVERRIDES.get(
1954
- `${elementName}:${slotKind}:${slotName}:${field.name}`,
1955
- );
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}`);
1956
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 };
1957
2308
  });
1958
2309
  }
1959
2310
 
@@ -2285,6 +2636,15 @@ function groupFlattenedTableFields(fields: readonly TableField[]): TableField[]
2285
2636
  return grouped;
2286
2637
  }
2287
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
+
2288
2648
  export function emitDeclarations(module: ApiModule, options?: EmitOptions): string {
2289
2649
  const prefix = `${module.namespace}.`;
2290
2650
 
@@ -2367,7 +2727,7 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2367
2727
  // A typedef that also has colon methods is emitted as a method-bearing
2368
2728
  // interface below, not an opaque brand alias.
2369
2729
  if (handleGroups.has(t.name)) continue;
2370
- lines.push(`${INDENT}${decl}type ${t.name} = Opaque<"${t.name}">;`);
2730
+ lines.push(`${INDENT}${decl}${typedefAlias(t.name, baseMapType)}`);
2371
2731
  }
2372
2732
  for (const aliasDecl of constantUnionAliasDeclarations(module.namespace)) {
2373
2733
  lines.push(`${INDENT}${decl}${aliasDecl}`);
@@ -2413,6 +2773,18 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2413
2773
  for (const docLine of functionDocLines(fn.original, translations)) lines.push(docLine);
2414
2774
  const line = emitFunction(fn, emitName, mapType, resolver, constantTokens, urlParameters);
2415
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
+ }
2416
2788
  }
2417
2789
 
2418
2790
  for (const alias of [...aliases].sort((a, b) => a.public.localeCompare(b.public))) {
@@ -2475,10 +2847,12 @@ export function emitDeclarations(module: ApiModule, options?: EmitOptions): stri
2475
2847
  const members = [...module.properties].sort((a, b) => a.name.localeCompare(b.name));
2476
2848
  lines.push(`${INDENT}${decl}interface properties {`);
2477
2849
  for (const p of members) {
2478
- for (const docLine of summaryDocLines(p.brief, p.description, `${INDENT}${INDENT}`)) {
2479
- 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}`);
2480
2855
  }
2481
- lines.push(`${INDENT}${INDENT}${emitPropertyMember(p, mapType, module.namespace)}`);
2482
2856
  }
2483
2857
  lines.push(`${INDENT}}`);
2484
2858
  }
@@ -2518,7 +2892,10 @@ export interface SymbolSignature {
2518
2892
  * {@link collectNestedGroups} tree the declaration emitter walks, so neither
2519
2893
  * surface can decide on its own which members are nested or how deep they go.
2520
2894
  * The caller applies the same `skipFunctions` filter the `.d.ts` generation
2521
- * 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.
2522
2899
  */
2523
2900
  export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions): SymbolSignature[] {
2524
2901
  const prefix = `${module.namespace}.`;
@@ -2558,19 +2935,28 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2558
2935
  const prepared = prepareFunction(fn, prefix);
2559
2936
  if (prepared === null) continue;
2560
2937
  const slotTypes: Record<string, string> = {};
2561
- out.push({
2562
- identity: fnIdentity(fn),
2563
- tsSignature: emitFunction(
2564
- prepared,
2565
- emitName(prepared.name),
2566
- mapType,
2567
- resolver,
2568
- constantTokens,
2569
- urlParameters,
2570
- slotTypes,
2571
- ),
2938
+ const primary = emitFunction(
2939
+ prepared,
2940
+ emitName(prepared.name),
2941
+ mapType,
2942
+ resolver,
2943
+ constantTokens,
2944
+ urlParameters,
2572
2945
  slotTypes,
2573
- });
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 });
2574
2960
  }
2575
2961
 
2576
2962
  const nested = flattenNestedGroups(collectNestedGroups(module, prefix));
@@ -2642,10 +3028,14 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2642
3028
  });
2643
3029
  }
2644
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>`.
2645
3034
  for (const p of module.properties) {
3035
+ const [firstMember = ""] = emitPropertyMembers(p, mapType, module.namespace);
2646
3036
  out.push({
2647
3037
  identity: { namespace: module.namespace, kind: "PROPERTY", name: p.name, signature: "" },
2648
- tsSignature: emitPropertyMember(p, mapType, module.namespace),
3038
+ tsSignature: firstMember,
2649
3039
  slotTypes: NO_SLOTS,
2650
3040
  });
2651
3041
  }
@@ -2654,7 +3044,7 @@ export function emitSymbolSignatures(module: ApiModule, options?: EmitOptions):
2654
3044
  if (!TS_IDENTIFIER.test(t.name) || handleGroups.has(t.name)) continue;
2655
3045
  out.push({
2656
3046
  identity: { namespace: module.namespace, kind: "TYPEDEF", name: t.name, signature: "" },
2657
- tsSignature: `type ${t.name} = Opaque<"${t.name}">;`,
3047
+ tsSignature: typedefAlias(t.name, baseMapType),
2658
3048
  slotTypes: NO_SLOTS,
2659
3049
  });
2660
3050
  }
@@ -2971,7 +3361,7 @@ function functionDocLines(
2971
3361
  ): string[] {
2972
3362
  const params = fn.parameters.map((p, index) => ({
2973
3363
  name: emittedParamName(p, index),
2974
- doc: htmlToDocText(p.doc),
3364
+ doc: rewriteParamDoc(fn.name, p.name, htmlToDocText(p.doc)),
2975
3365
  }));
2976
3366
  const onlyReturn = fn.returnValues.length === 1 ? fn.returnValues[0] : undefined;
2977
3367
  const lua = htmlToCodeText(fn.examples ?? "");
@@ -3102,19 +3492,22 @@ function parameterType(
3102
3492
  // `safeParamName` fallback.
3103
3493
  const alias = SCENE_ADDRESS_ALIASES[classifyUrlParameter(urlParameters, elementName, p.name)];
3104
3494
  const slotMapType = alias === undefined ? mapType : addressMapType(mapType, alias);
3105
- return concrete.length > 0
3106
- ? mapSlotUnion(
3107
- concrete,
3108
- p.doc,
3109
- slotMapType,
3110
- true,
3111
- resolver,
3112
- constantTokens,
3113
- elementName,
3114
- "param",
3115
- p.name,
3116
- )
3117
- : "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}`;
3118
3511
  }
3119
3512
 
3120
3513
  const VARARG_PREFIX = "...";
@@ -3263,12 +3656,19 @@ function emitReturn(
3263
3656
  return { type: ts, trailing: "", slots: [{ position: 0, name: first.name, ts }] };
3264
3657
  }
3265
3658
 
3266
- function emitPropertyMember(
3659
+ function emitPropertyMembers(
3267
3660
  p: ApiProperty,
3268
3661
  mapType: (t: string) => string,
3269
3662
  namespace: string,
3270
- ): string {
3271
- 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
+ }
3272
3672
  const correction = PROPERTY_TYPE_CORRECTIONS.get(`${namespace}.${p.name}`);
3273
3673
  const ts =
3274
3674
  correction !== undefined
@@ -3276,7 +3676,7 @@ function emitPropertyMember(
3276
3676
  : p.types.length > 0
3277
3677
  ? unionFromTokens(p.types, mapType)
3278
3678
  : "unknown";
3279
- return `${key}: ${ts};`;
3679
+ return keys.map((key) => `${key}: ${ts};`);
3280
3680
  }
3281
3681
 
3282
3682
  // Like `unionFromTokens`, but a `table` token whose slot doc carries a parseable
@@ -3341,11 +3741,21 @@ function mapSlotUnion(
3341
3741
  // A mapping key may be a `A | B` union token too (render.clear's three
3342
3742
  // documented graphics.BUFFER_TYPE_* constants); split it the same way
3343
3743
  // the value above is split. A single-token key (no `|`) is unaffected.
3344
- const key = unionFromTokens(
3345
- mapping.key.split("|").map((token) => token.trim()),
3346
- mapType,
3347
- );
3348
- 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
+ }
3349
3759
  } else if (element !== undefined) {
3350
3760
  ts = arrayTypeFromTokens(element, mapType);
3351
3761
  } else if (curation?.kind === "object" || curation?.kind === "array-object") {