@defold-typescript/types 0.36.0 → 0.38.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.
@@ -294,7 +294,7 @@ declare global {
294
294
  * }
295
295
  * ```
296
296
  */
297
- function get_ifaddrs(): { name: string; address: string; mac: string; up: boolean; running: boolean }[];
297
+ function get_ifaddrs(): { name: string; address?: string; mac?: string; up: boolean; running: boolean }[];
298
298
  /**
299
299
  * The save-file path is operating system specific and is typically located under the user's home directory.
300
300
  * This function will raise a Lua error if unable to get the save file path.
@@ -365,7 +365,7 @@ declare global {
365
365
  * }
366
366
  * ```
367
367
  */
368
- function get_sys_info(options?: { ignore_secure?: boolean }): { device_model: string; manufacturer: string; system_name: string; system_version: string; api_version: string; language: string; device_language: string; territory: string; gmt_offset: number; device_ident: string; user_agent: string };
368
+ function get_sys_info(options?: { ignore_secure?: boolean }): { device_model?: string; manufacturer?: string; system_name: string; system_version: string; api_version: string; language: string; device_language: string; territory: string; gmt_offset: number; device_ident?: string; user_agent?: string };
369
369
  /**
370
370
  * If the file exists, it must have been created by `sys.save` to be loaded.
371
371
  * This function will raise a Lua error if an error occurs while loading the file.
@@ -95,7 +95,7 @@ declare global {
95
95
  *
96
96
  * - number `inset_bottom`
97
97
  */
98
- function get_safe_area(): { safe_area: { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number } };
98
+ function get_safe_area(): { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number };
99
99
  /**
100
100
  * This returns the current window size (width and height).
101
101
  */
@@ -14,7 +14,7 @@ declare global {
14
14
  * @param url - the url to the game object collision component
15
15
  * @returns the body if successful. Otherwise `nil`.
16
16
  */
17
- function get_body(url: string | Hash | Url): Opaque<"b2Body">;
17
+ function get_body(url: string | Hash | Url): Opaque<"b2Body"> | undefined;
18
18
  /**
19
19
  * Get the Box2D version information for the active backend.
20
20
  *
@@ -133,10 +133,12 @@ declare global {
133
133
  * @example
134
134
  * ```ts
135
135
  * const body = b2d.get_body("#collisionobject");
136
- * const triangle = b2d.body.create_fixture(body, {
137
- * density: 1.0, friction: 0.3,
138
- * shape: { type: b2d.shape.SHAPE_TYPE_POLYGON, vertices: [vmath.vector3(-16, -16, 0), vmath.vector3(16, -16, 0), vmath.vector3(0, 16, 0)] },
139
- * });
136
+ * if (body !== undefined) {
137
+ * const triangle = b2d.body.create_fixture(body, {
138
+ * density: 1.0, friction: 0.3,
139
+ * shape: { type: b2d.shape.SHAPE_TYPE_POLYGON, vertices: [vmath.vector3(-16, -16, 0), vmath.vector3(16, -16, 0), vmath.vector3(0, 16, 0)] },
140
+ * });
141
+ * }
140
142
  * ```
141
143
  */
142
144
  function create_fixture(body: Opaque<"b2Body">, definition: { shape?: { type?: number; radius?: number; center?: Vector3; v0?: Vector3; v1?: Vector3; v2?: Vector3; v3?: Vector3; vertices?: Vector3[]; hx?: number; hy?: number; angle?: number; loop?: boolean; prev_vertex?: Vector3; next_vertex?: Vector3 }; friction?: number; restitution?: number; density?: number; sensor?: boolean; filter?: { category_bits?: number; mask_bits?: number; group_index?: number } }): { index: number; type: number; sensor: boolean; density: number; friction: number; restitution: number; child_count: number };
@@ -134,10 +134,12 @@ declare global {
134
134
  * @example
135
135
  * ```ts
136
136
  * const body = b2d.get_body("#collisionobject");
137
- * const circle = b2d.fixture.get_shape(body, 1);
138
- * circle.center = vmath.vector3(24, 0, 0);
139
- * b2d.fixture.set_shape(body, 1, circle, true);
140
- * b2d.fixture.set_shape(body, 2, { type: b2d.shape.SHAPE_TYPE_EDGE, v1: vmath.vector3(-32, 0, 0), v2: vmath.vector3(32, 0, 0) });
137
+ * if (body !== undefined) {
138
+ * const circle = b2d.fixture.get_shape(body, 1);
139
+ * circle.center = vmath.vector3(24, 0, 0);
140
+ * b2d.fixture.set_shape(body, 1, circle, true);
141
+ * b2d.fixture.set_shape(body, 2, { type: b2d.shape.SHAPE_TYPE_EDGE, v1: vmath.vector3(-32, 0, 0), v2: vmath.vector3(32, 0, 0) });
142
+ * }
141
143
  * ```
142
144
  */
143
145
  function set_shape(body: Opaque<"b2Body">, fixture_index: number, shape: { type?: number; radius?: number; center?: Vector3; v0?: Vector3; v1?: Vector3; v2?: Vector3; v3?: Vector3; vertices?: Vector3[]; hx?: number; hy?: number; angle?: number; loop?: boolean; prev_vertex?: Vector3; next_vertex?: Vector3 }, update_mass?: boolean): void;
@@ -202,10 +202,12 @@ declare global {
202
202
  * @example
203
203
  * ```ts
204
204
  * const body = b2d.get_body("#collisionobject");
205
- * const circle = b2d.shape.get_shape(body, 1);
206
- * circle.center = vmath.vector3(24, 0, 0);
207
- * b2d.shape.set_shape(body, 1, circle, true);
208
- * b2d.shape.set_shape(body, 2, { type: b2d.shape.SHAPE_TYPE_SEGMENT, v1: vmath.vector3(-32, 0, 0), v2: vmath.vector3(32, 0, 0) });
205
+ * if (body !== undefined) {
206
+ * const circle = b2d.shape.get_shape(body, 1);
207
+ * circle.center = vmath.vector3(24, 0, 0);
208
+ * b2d.shape.set_shape(body, 1, circle, true);
209
+ * b2d.shape.set_shape(body, 2, { type: b2d.shape.SHAPE_TYPE_SEGMENT, v1: vmath.vector3(-32, 0, 0), v2: vmath.vector3(32, 0, 0) });
210
+ * }
209
211
  * ```
210
212
  */
211
213
  function set_shape(shape_id: Opaque<"b2Shape">, definition: { type?: number; radius?: number; center?: Vector3; v0?: Vector3; v1?: Vector3; v2?: Vector3; v3?: Vector3; vertices?: Vector3[]; hx?: number; hy?: number; angle?: number; loop?: boolean; prev_vertex?: Vector3; next_vertex?: Vector3 }, update_mass: boolean): void;
@@ -844,7 +844,7 @@ declare global {
844
844
  * // the info table contains meta data about all the render target attachments
845
845
  * // so it's not necessary to use resource.get_texture here, but we do it here
846
846
  * // just to show that it's possible:
847
- * const info_attachment_1 = resource.get_texture_info(info.attachments[0].handle);
847
+ * const info_attachment_1 = resource.get_texture_info(info.attachments[0]!.handle);
848
848
  * },
849
849
  * });
850
850
  *
@@ -852,7 +852,7 @@ declare global {
852
852
  * export default defineScript({
853
853
  * init() {
854
854
  * const info = resource.get_render_target_info("/my_render_target.render_targetc");
855
- * const attachment = info.attachments[0].texture;
855
+ * const attachment = info.attachments[0]!.texture!;
856
856
  * // you can also get texture info from the 'texture' field, since it's a resource hash
857
857
  * const texture_info = resource.get_texture_info(attachment);
858
858
  * go.set("#model", "texture0", attachment);
@@ -860,7 +860,7 @@ declare global {
860
860
  * });
861
861
  * ```
862
862
  */
863
- function get_render_target_info(path: Hash | string | number): { handle: number; width: number; height: number; depth: number; mipmaps: number; type: number; buffer_type: number; texture: Hash };
863
+ function get_render_target_info(path: Hash | string | number): { handle: number; attachments: { handle: number; width: number; height: number; depth: number; mipmaps: number; type: number; buffer_type: number; texture?: Hash }[] };
864
864
  /**
865
865
  * Gets the text metrics from a font
866
866
  *
@@ -1051,7 +1051,7 @@ declare global {
1051
1051
  * export default defineScript({
1052
1052
  * init(self) {
1053
1053
  * const rt_info = resource.get_render_target_info(self.my_render_target);
1054
- * go.set("#model", "texture0", rt_info.attachments[0].texture);
1054
+ * go.set("#model", "texture0", rt_info.attachments[0]!.texture!);
1055
1055
  * },
1056
1056
  * });
1057
1057
  * ```
@@ -294,7 +294,7 @@ declare global {
294
294
  * }
295
295
  * ```
296
296
  */
297
- function get_ifaddrs(): { name: string; address: string; mac: string; up: boolean; running: boolean }[];
297
+ function get_ifaddrs(): { name: string; address?: string; mac?: string; up: boolean; running: boolean }[];
298
298
  /**
299
299
  * The save-file path is operating system specific and is typically located under the user's home directory.
300
300
  * This function will raise a Lua error if unable to get the save file path.
@@ -365,7 +365,7 @@ declare global {
365
365
  * }
366
366
  * ```
367
367
  */
368
- function get_sys_info(options?: { ignore_secure?: boolean }): { device_model: string; manufacturer: string; system_name: string; system_version: string; api_version: string; language: string; device_language: string; territory: string; gmt_offset: number; device_ident: string; user_agent: string };
368
+ function get_sys_info(options?: { ignore_secure?: boolean }): { device_model?: string; manufacturer?: string; system_name: string; system_version: string; api_version: string; language: string; device_language: string; territory: string; gmt_offset: number; device_ident?: string; user_agent?: string };
369
369
  /**
370
370
  * If the file exists, it must have been created by `sys.save` to be loaded.
371
371
  * This function will raise a Lua error if an error occurs while loading the file.
@@ -95,7 +95,7 @@ declare global {
95
95
  *
96
96
  * - number `inset_bottom`
97
97
  */
98
- function get_safe_area(): { safe_area: { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number } };
98
+ function get_safe_area(): { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number };
99
99
  /**
100
100
  * This returns the current window size (width and height).
101
101
  */
@@ -95,7 +95,7 @@ declare global {
95
95
  *
96
96
  * - number `inset_bottom`
97
97
  */
98
- function get_safe_area(): { safe_area: { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number } };
98
+ function get_safe_area(): { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number };
99
99
  /**
100
100
  * This returns the current window size (width and height).
101
101
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/emit-dts.ts CHANGED
@@ -245,6 +245,32 @@ export const RETURN_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
245
245
  ["editor.tx.add", 'Opaque<"transaction_step">'],
246
246
  ]);
247
247
 
248
+ // A function whose `returnvalues` declare a concrete type while the same slot's
249
+ // prose documents a `nil` alternative. Distinct from RETURN_TYPE_OVERRIDES
250
+ // above, which fills an *empty* `returnvalues`: these contradict a token
251
+ // upstream explicitly declares, so each entry records it and
252
+ // `return-correction-provenance.test.ts` pins it against every vendored
253
+ // ref-doc. Upstream adding `nil` to `types` hands the case to the top-level
254
+ // `nil` projection in `mapSlotUnion` and reds the entry, which is then deleted
255
+ // rather than re-pinned.
256
+ export interface ReturnTypeCorrection {
257
+ readonly ts: string;
258
+ readonly upstream: readonly string[];
259
+ readonly reason: string;
260
+ }
261
+
262
+ // Keyed by FQN, mirroring PROPERTY_TYPE_CORRECTIONS.
263
+ export const RETURN_TYPE_CORRECTIONS: ReadonlyMap<string, ReturnTypeCorrection> = new Map([
264
+ [
265
+ "b2d.get_body",
266
+ {
267
+ ts: 'Opaque<"b2Body"> | undefined',
268
+ upstream: ["b2Body"],
269
+ reason: "the body if successful. Otherwise nil.",
270
+ },
271
+ ],
272
+ ]);
273
+
248
274
  // A property whose upstream `<span class="type">` states a type the engine does
249
275
  // not use. Unlike every other override here, which fills a gap upstream left
250
276
  // empty, these contradict a token upstream explicitly declares — so each entry
@@ -819,6 +845,38 @@ export const TABLE_SLOT_CURATIONS: ReadonlyMap<string, TableSlotCuration> = new
819
845
  ],
820
846
  },
821
847
  ],
848
+ // resource.get_render_target_info's return `<dl>` writes one key as
849
+ // `<dt>'attachments'</dt>` — quoted rather than wrapped in `<code>` — so
850
+ // TABLE_FIELD drops it and parseTableFields hoists the seven per-attachment
851
+ // fields to the top level, contradicting the `info.attachments[0].handle`
852
+ // access in the doc's own `@example`. Same shape and same remedy as
853
+ // font.get_info:return:info above: replace the mis-parsed field list with the
854
+ // faithful nested isList object. `texture` is documented "only available if
855
+ // the render target passed in is a resource", hence optional.
856
+ [
857
+ "resource.get_render_target_info:return:table",
858
+ {
859
+ kind: "object",
860
+ fields: [
861
+ { name: "handle", types: ["number"] },
862
+ {
863
+ name: "attachments",
864
+ types: ["table"],
865
+ isList: true,
866
+ fields: [
867
+ { name: "handle", types: ["number"] },
868
+ { name: "width", types: ["number"] },
869
+ { name: "height", types: ["number"] },
870
+ { name: "depth", types: ["number"] },
871
+ { name: "mipmaps", types: ["number"] },
872
+ { name: "type", types: ["number"] },
873
+ { name: "buffer_type", types: ["number"] },
874
+ { name: "texture", types: ["hash"], optional: true },
875
+ ],
876
+ },
877
+ ],
878
+ },
879
+ ],
822
880
  // profiler.view_recorded_frame's `frame_index` is a typed `<ul>` "specify one
823
881
  // of the following": `distance` and `frame`, both numeric frame offsets. The
824
882
  // <li> items carry no `<span class="type">`, so parseTableFields recovers
@@ -1369,6 +1427,95 @@ export const TABLE_FIELD_TYPE_OVERRIDES: ReadonlyMap<string, string> = new Map([
1369
1427
  ["material.get_vertex_attributes:return:table:value", ATTRIBUTE_VALUE_TS],
1370
1428
  ]);
1371
1429
 
1430
+ // A return-slot field upstream documents as platform-gated or nil-valued while
1431
+ // declaring it like any other. Lua removes a key assigned `nil`, so "might be
1432
+ // nil if not available" and "only available on iOS and Android" describe the
1433
+ // same runtime state — the key is absent — and the correction is always `?`,
1434
+ // never a required field widened to `| undefined`.
1435
+ //
1436
+ // Separate from TABLE_FIELD_TYPE_OVERRIDES because presence is the other axis:
1437
+ // the parser reads these fields' types correctly and stays authoritative for
1438
+ // them, so an entry here restates no type and cannot drift when upstream
1439
+ // retypes a field. `upstream` is pinned rather than consumed, and `evidence` is
1440
+ // the decoded prose `return-correction-provenance.test.ts` searches the slot doc
1441
+ // for; both red the entry once upstream marks the field or drops the gate.
1442
+ //
1443
+ // Keyed `<element>:<kind>:<slot>:<field>`, the TABLE_FIELD_TYPE_OVERRIDES shape.
1444
+ export interface ReturnFieldOptionalityCorrection {
1445
+ readonly upstream: readonly string[];
1446
+ readonly evidence: string;
1447
+ }
1448
+
1449
+ // The platform-gate markers upstream renders as `<span class="icon-*">` decode
1450
+ // to `[ios]`, `[android]`, `[html5]` — `user_agent` states its gate through the
1451
+ // marker alone, with no availability sentence to pin.
1452
+ export const RETURN_FIELD_OPTIONALITY_CORRECTIONS: ReadonlyMap<
1453
+ string,
1454
+ ReturnFieldOptionalityCorrection
1455
+ > = new Map([
1456
+ [
1457
+ "sys.get_sys_info:return:sys_info:device_model",
1458
+ { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
1459
+ ],
1460
+ [
1461
+ "sys.get_sys_info:return:sys_info:manufacturer",
1462
+ { upstream: ["string"], evidence: "[ios][android] Only available on iOS and Android." },
1463
+ ],
1464
+ [
1465
+ "sys.get_sys_info:return:sys_info:device_ident",
1466
+ {
1467
+ upstream: ["string"],
1468
+ evidence: '[ios] "identifierForVendor" on iOS. [android] "android_id" on Android.',
1469
+ },
1470
+ ],
1471
+ [
1472
+ "sys.get_sys_info:return:sys_info:user_agent",
1473
+ { upstream: ["string"], evidence: "[html5] The HTTP user agent" },
1474
+ ],
1475
+ [
1476
+ "sys.get_ifaddrs:return:ifaddrs:address",
1477
+ { upstream: ["string"], evidence: "might be nil if not available." },
1478
+ ],
1479
+ [
1480
+ "sys.get_ifaddrs:return:ifaddrs:mac",
1481
+ { upstream: ["string"], evidence: "might be nil if not available." },
1482
+ ],
1483
+ ]);
1484
+
1485
+ // Mark a parser-recovered field optional from RETURN_FIELD_OPTIONALITY_CORRECTIONS,
1486
+ // leaving every other field — and every field's type — untouched. Sets the
1487
+ // `optional` flag `TableField` already carries and `inlineTableType` already
1488
+ // reads, so no `mapType` is needed and no correction can change a type.
1489
+ //
1490
+ // A correction keyed to this slot that matches no recovered field throws rather
1491
+ // than no-opping: a field upstream renamed would otherwise leave a stale entry
1492
+ // looking applied while the emitted declaration silently re-widened.
1493
+ // Returns a new array; never mutates the parser's result.
1494
+ export function applyFieldOptionalityCorrections(
1495
+ elementName: string,
1496
+ slotKind: "param" | "return" | undefined,
1497
+ slotName: string | undefined,
1498
+ fields: readonly TableField[],
1499
+ ): TableField[] {
1500
+ if (slotKind === undefined || slotName === undefined) return [...fields];
1501
+ const prefix = `${tableSlotKey(elementName, slotKind, slotName)}:`;
1502
+ const corrected = fields.map((field) =>
1503
+ RETURN_FIELD_OPTIONALITY_CORRECTIONS.has(`${prefix}${field.name}`)
1504
+ ? { ...field, optional: true }
1505
+ : field,
1506
+ );
1507
+ const recovered = new Set(fields.map((field) => field.name));
1508
+ const stale = [...RETURN_FIELD_OPTIONALITY_CORRECTIONS.keys()].filter(
1509
+ (key) => key.startsWith(prefix) && !recovered.has(key.slice(prefix.length)),
1510
+ );
1511
+ if (stale.length > 0) {
1512
+ throw new Error(
1513
+ `field-optionality correction names no recovered field: ${stale.sort().join(", ")}`,
1514
+ );
1515
+ }
1516
+ return corrected;
1517
+ }
1518
+
1372
1519
  // Pin a parser-recovered field's TS type from TABLE_FIELD_TYPE_OVERRIDES,
1373
1520
  // leaving every other field parser-authoritative. A field the override does not
1374
1521
  // name is returned unchanged; a field it names gains a `tsType` (emitted
@@ -1605,7 +1752,11 @@ export function buildTableDocResolver(sources: readonly TableDocSource[]): Table
1605
1752
  return (elementName) => docByName.get(elementName);
1606
1753
  }
1607
1754
 
1608
- export function parseTableFields(doc: string, resolver?: TableDocResolver): TableField[] | null {
1755
+ export function parseTableFields(
1756
+ doc: string,
1757
+ resolver?: TableDocResolver,
1758
+ slotName?: string,
1759
+ ): TableField[] | null {
1609
1760
  const fields: TableField[] = [];
1610
1761
  for (const match of doc.matchAll(TABLE_FIELD)) {
1611
1762
  const name = (match[1] ?? "").trim();
@@ -1624,14 +1775,20 @@ export function parseTableFields(doc: string, resolver?: TableDocResolver): Tabl
1624
1775
  }
1625
1776
  // Nested recovery, scoped to the one unambiguous shape: a `<dl>` declaring a
1626
1777
  // single `table`-typed field whose keys sit in an immediately-following
1627
- // top-level `<ul>` typed-field list (`window.get_safe_area`). Attach the
1628
- // `<ul>` fields as that field's nested shape so the slot recovers as a nested
1629
- // object instead of a `Record`.
1778
+ // top-level `<ul>` typed-field list. Attach the `<ul>` fields as that field's
1779
+ // nested shape so the slot recovers as a nested object instead of a `Record`.
1780
+ //
1781
+ // Unless the header restates the slot it describes (`window.get_safe_area`'s
1782
+ // `safe_area` return documented under a `safe_area` header): that is a
1783
+ // restatement, not a nesting level, and wrapping it emits a level the engine
1784
+ // never returns. Only a caller that knows the slot name can tell the two
1785
+ // apart, so the `<ul>` keys are returned directly when they match.
1630
1786
  if (fields.length === 1) {
1631
1787
  const only = fields[0];
1632
1788
  if (only && only.types.length === 1 && only.types[0] === "table") {
1633
1789
  const nested = parseUlFields(doc);
1634
1790
  if (nested.length > 0) {
1791
+ if (slotName !== undefined && slotName === only.name) return nested;
1635
1792
  only.fields = nested;
1636
1793
  return [only];
1637
1794
  }
@@ -2586,6 +2743,10 @@ function emitReturn(
2586
2743
  // slots and leaves the consumer on its token-derived fallback.
2587
2744
  const override = RETURN_TYPE_OVERRIDES.get(elementName);
2588
2745
  if (override !== undefined) return { type: override, trailing: "", slots: [] };
2746
+ // Kept beside the override lookup so the two stay visibly distinct: this one
2747
+ // contradicts a declared token rather than filling an empty `returnvalues`,
2748
+ // and it is a per-slot render, so it reports its slot.
2749
+ const correction = RETURN_TYPE_CORRECTIONS.get(elementName);
2589
2750
  if (returnValues.length === 0) return { type: "void", trailing: "", slots: [] };
2590
2751
  if (returnValues.length > 1) {
2591
2752
  // Defold multi-returns are positional and always present; each slot maps
@@ -2617,6 +2778,13 @@ function emitReturn(
2617
2778
  }
2618
2779
  const first = returnValues[0];
2619
2780
  if (!first) return { type: "void", trailing: "", slots: [] };
2781
+ if (correction !== undefined) {
2782
+ return {
2783
+ type: correction.ts,
2784
+ trailing: "",
2785
+ slots: [{ position: 0, name: first.name, ts: correction.ts }],
2786
+ };
2787
+ }
2620
2788
  const ts =
2621
2789
  first.types.length > 0
2622
2790
  ? mapSlotUnion(
@@ -2727,19 +2895,25 @@ function mapSlotUnion(
2727
2895
  // arbitrary name. Reusing the parser keeps the inner fields in lockstep
2728
2896
  // with the doc instead of a hand-listed drift-prone copy; a documented
2729
2897
  // array-form field is pinned via TABLE_FIELD_TYPE_OVERRIDES.
2730
- const parsed = applyFieldTypeOverrides(
2898
+ const parsed = applyFieldOptionalityCorrections(
2731
2899
  elementName,
2732
2900
  slotKind,
2733
2901
  slotName,
2734
- parseTableFields(doc, resolver) ?? [],
2902
+ applyFieldTypeOverrides(
2903
+ elementName,
2904
+ slotKind,
2905
+ slotName,
2906
+ parseTableFields(doc, resolver, slotName) ?? [],
2907
+ ),
2735
2908
  );
2736
2909
  const object = inlineTableType(parsed, mapType, optionalFields);
2737
2910
  ts = `Record<string, ${object}>`;
2738
2911
  } else {
2739
- const parsed = parseTableFields(doc, resolver);
2912
+ const parsed = parseTableFields(doc, resolver, slotName);
2740
2913
  if (parsed !== null) {
2741
2914
  const nested = applyNestedFieldCurations(elementName, slotKind, slotName, parsed);
2742
- const fields = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
2915
+ const typed = applyFieldTypeOverrides(elementName, slotKind, slotName, nested);
2916
+ const fields = applyFieldOptionalityCorrections(elementName, slotKind, slotName, typed);
2743
2917
  const object = inlineTableType(fields, mapType, optionalFields);
2744
2918
  ts = isSlotLevelList(doc) ? `${object}[]` : object;
2745
2919
  } else {
package/src/lifecycle.ts CHANGED
@@ -571,6 +571,14 @@ export type RenderScriptHooksWithProperties<TProps, TSelf, TInitState> = Omit<
571
571
  properties?: TProps;
572
572
  };
573
573
 
574
+ // The script-local message map an `onMessage` dispatcher carries, lifted onto
575
+ // the factory result so `ScriptMessages<typeof script>` can read it. Matched
576
+ // structurally so this module stays usable through its own `./lifecycle`
577
+ // subpath, where the ambient `MessageDispatcher` is not declared.
578
+ export type ScriptMessageChannel<TMessages> = {
579
+ on_message?: { readonly __messages?: TMessages };
580
+ };
581
+
574
582
  /**
575
583
  * Extract a script module's declared property channel (`TProps`) as a nameable
576
584
  * type. A script declares its editor properties with the value-keyed
@@ -617,9 +625,15 @@ export type ScriptPropertiesOf<T extends { properties?: object }> = NonNullable<
617
625
  * });
618
626
  * ```
619
627
  */
620
- export function defineScript<TProps extends object = Record<never, never>, TInitState = TProps>(
621
- hooks: ScriptHooksWithProperties<TProps, TProps & TInitState, TInitState>,
622
- ): ScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> {
628
+ export function defineScript<
629
+ TProps extends object = Record<never, never>,
630
+ TInitState = TProps,
631
+ TMessages extends object = Record<never, never>,
632
+ >(
633
+ hooks: ScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
634
+ ScriptMessageChannel<TMessages>,
635
+ ): ScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
636
+ ScriptMessageChannel<TMessages> {
623
637
  return hooks;
624
638
  }
625
639
 
@@ -652,9 +666,15 @@ export function defineScript<TProps extends object = Record<never, never>, TInit
652
666
  * });
653
667
  * ```
654
668
  */
655
- export function defineGuiScript<TProps extends object = Record<never, never>, TInitState = TProps>(
656
- hooks: GuiScriptHooksWithProperties<TProps, TProps & TInitState, TInitState>,
657
- ): GuiScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> {
669
+ export function defineGuiScript<
670
+ TProps extends object = Record<never, never>,
671
+ TInitState = TProps,
672
+ TMessages extends object = Record<never, never>,
673
+ >(
674
+ hooks: GuiScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
675
+ ScriptMessageChannel<TMessages>,
676
+ ): GuiScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
677
+ ScriptMessageChannel<TMessages> {
658
678
  return hooks;
659
679
  }
660
680
 
@@ -689,8 +709,11 @@ export function defineGuiScript<TProps extends object = Record<never, never>, TI
689
709
  export function defineRenderScript<
690
710
  TProps extends object = Record<never, never>,
691
711
  TInitState = TProps,
712
+ TMessages extends object = Record<never, never>,
692
713
  >(
693
- hooks: RenderScriptHooksWithProperties<TProps, TProps & TInitState, TInitState>,
694
- ): RenderScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> {
714
+ hooks: RenderScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
715
+ ScriptMessageChannel<TMessages>,
716
+ ): RenderScriptHooksWithProperties<TProps, TProps & TInitState, TInitState> &
717
+ ScriptMessageChannel<TMessages> {
695
718
  return hooks;
696
719
  }
@@ -3,8 +3,9 @@ import type { Hash, Url } from "./core-types";
3
3
 
4
4
  declare global {
5
5
  // DU-style dispatcher built on `isMessage`'s receive-side narrowing
6
- // (message-guard.d.ts): each handler key is a `BuiltinMessageId` and its
7
- // `message` param narrows to that id's `BuiltinMessages` payload. It returns
6
+ // (message-guard.d.ts): each catalog handler key is a `MessageId` and its
7
+ // `message` param narrows to that id's `MessagePayload`; any other key is a
8
+ // script-local id declared by its annotated `message`. It returns
8
9
  // an `on_message` handler so it reads as `on_message: onMessage<Self>({ ... })`;
9
10
  // `self` threads via the explicit type argument, mirroring `defineScript<Self>`.
10
11
  // The transpiler lowers the call to a flat `message_id == hash("...")`
@@ -18,7 +19,11 @@ declare global {
18
19
  * explicit type argument, mirroring `defineScript<Self>`. The transpiler lowers
19
20
  * it to a flat `if/elseif message_id == hash("...")` chain.
20
21
  *
21
- * @param handlers - a partial map from builtin message id to its handler; each handler's `message` is narrowed to that id's payload.
22
+ * A key outside the catalogs declares a script-local message when its handler
23
+ * annotates `message`; the returned dispatcher carries those ids so
24
+ * {@link ScriptMessages} can hand them to senders.
25
+ *
26
+ * @param handlers - a partial map from message id to its handler; a catalog id's `message` is narrowed to that id's payload, and an annotated handler under any other key declares a script-local id.
22
27
  * @returns the `on_message` lifecycle handler that dispatches to the matching entry.
23
28
  * @example
24
29
  * ```ts
@@ -27,18 +32,55 @@ declare global {
27
32
  * contact_point_response(self, message, sender) {
28
33
  * print(message.other_group);
29
34
  * },
35
+ * spawn_wave(self, message: { count: number }) {
36
+ * print(message.count);
37
+ * },
30
38
  * }),
31
39
  * });
32
40
  * ```
33
41
  */
34
- function onMessage<TSelf = Record<never, never>>(
35
- handlers: Partial<{
36
- [K in MessageId]: (self: TSelf, message: MessagePayload<K>, sender: Url) => void;
37
- }>,
38
- ): (
39
- self: TSelf,
40
- message_id: Hash,
41
- message: Record<string | number, unknown>,
42
- sender: Url,
43
- ) => void;
42
+ function onMessage<TSelf = Record<never, never>, H extends object = Record<never, never>>(
43
+ handlers: H &
44
+ Partial<{
45
+ [K in MessageId]: (self: TSelf, message: MessagePayload<K>, sender: Url) => void;
46
+ }> &
47
+ OnMessageLocalHandlers<TSelf, H>,
48
+ ): MessageDispatcher<TSelf, OnMessageLocalMessages<H>>;
49
+
50
+ // A key outside `MessageId` is a script-local id only when its handler
51
+ // annotates `message`; an unannotated one infers no payload and reds as an
52
+ // implicit `any`, which is the typo guard.
53
+ type OnMessageHandlerPayload<F> = F extends (
54
+ self: never,
55
+ message: infer M,
56
+ ...rest: never[]
57
+ ) => unknown
58
+ ? M
59
+ : never;
60
+
61
+ type OnMessageLocalHandlers<TSelf, H> = {
62
+ [K in Exclude<keyof H, MessageId>]: (
63
+ self: TSelf,
64
+ message: OnMessageHandlerPayload<H[K]>,
65
+ sender: Url,
66
+ ) => void;
67
+ };
68
+
69
+ // A numeric key is sent under its JavaScript property key (`1e3` is "1000"),
70
+ // the id the transpiler hashes.
71
+ type OnMessageLocalMessages<H> = {
72
+ [K in Exclude<keyof H, MessageId> as K extends number ? `${K}` : K]: OnMessageHandlerPayload<
73
+ H[K]
74
+ >;
75
+ };
76
+
77
+ /**
78
+ * The `on_message` handler {@link onMessage} returns. `__messages` is a
79
+ * type-only record of the script-local ids its handlers declared; it never
80
+ * exists at runtime.
81
+ */
82
+ interface MessageDispatcher<TSelf, TLocal = Record<never, never>> {
83
+ (self: TSelf, message_id: Hash, message: Record<string | number, unknown>, sender: Url): void;
84
+ readonly __messages?: TLocal;
85
+ }
44
86
  }