@hopple/sdk 0.1.0-rc.3 → 1.0.0-next.1

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.
package/index.d.ts CHANGED
@@ -1,34 +1,4 @@
1
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/achievement.d.ts
2
- /**
3
- * The largest value progress will carry.
4
- *
5
- * Spelled here *and* in `@hopple/manifest`, on purpose and with a test to keep
6
- * them equal — the server checks the same rule and is not allowed to depend on
7
- * this package.
8
- *
9
- * @group Achievements
10
- */
11
- declare const ACHIEVEMENT_VALUE_MAX = 9007199254740991;
12
- /**
13
- * What the host says came of a report.
14
- *
15
- * `earned` is the transition and the only thing worth drawing a banner on: a
16
- * game that unlocks the same achievement on every level-up gets it once. A host
17
- * that took the report answers both fields from this player's own history, so
18
- * unlike a score's rank there is nothing here a host might not know.
19
- *
20
- * @group Achievements
21
- */
22
- type AchievementResult = {
23
- /** Whether it is theirs, after this call. */
24
- unlocked: boolean;
25
- /** Whether *this call* is what unlocked it. False for one already held. */
26
- earned: boolean;
27
- /** The best value, after this call. Absent for a flagged achievement. */
28
- progress?: number;
29
- };
30
- //#endregion
31
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/analytics.d.ts
1
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/analytics.d.ts
32
2
  /**
33
3
  * What a game says happened.
34
4
  *
@@ -59,166 +29,77 @@ type AnalyticsValue = string | number | boolean;
59
29
  /** @group Analytics */
60
30
  type AnalyticsProps = Record<string, AnalyticsValue>;
61
31
  //#endregion
62
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/audio.d.ts
63
- /**
64
- * How loud the game is allowed to be, which is the host's to decide and the
65
- * game's to obey.
66
- *
67
- * **This is the one namespace where the host is not offering a service.** A
68
- * save, a score, a link and an achievement are all things a game asks for and a
69
- * host may refuse; audio runs the other way. The host states the volumes, and a
70
- * game that ignores them is a game that plays music over a phone call, under an
71
- * advertisement, and in a library. So there is no `unsupported` here and no way
72
- * to decline: the only thing a game does with this is apply it.
73
- *
74
- * **Nothing about sound crosses the bridge.** The samples, the mixing and the
75
- * decoding all happen in the frame, exactly as presses do — what travels is the
76
- * three numbers the frame cannot know, because they belong to a player using a
77
- * host rather than to a game.
78
- *
79
- * **The channels are named by the manifest and not described here.** What
80
- * `music` means is the game's, and the host learned the id and its title from
81
- * the file it was published with. So the wire carries volumes keyed by id and
82
- * nothing else, and a host newer than a game — or a game whose next version
83
- * renamed a channel — produces an id the other side simply has no fader for.
84
- */
32
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/errors.d.ts
85
33
  /**
86
- * The loudest anything gets, and the quietest.
87
- *
88
- * A fraction rather than decibels, because it is what a fader hands over and
89
- * what a gain node takes, and neither end of this wire is the place to decide
90
- * on a curve. A host that wants one applies it before it says a number; a game
91
- * that wants one applies it after. Both are right, and the wire stays a
92
- * fraction either way.
34
+ * How a call fails.
93
35
  *
94
- * @group Audio
95
- */
96
- declare const AUDIO_VOLUME_MAX = 1;
97
- /**
98
- * The channel every host has and no game declares. See `AUDIO_MASTER` in
99
- * `@hopple/manifest`, which is the same word said where a manifest can be
100
- * checked against it.
36
+ * Requests reject; nothing throws synchronously once a peer exists. The codes
37
+ * are the stable part — a game built against a newer SDK than the host it lands
38
+ * on must be able to tell why it was refused and degrade, rather than take an
39
+ * unhandled rejection into the player's face.
101
40
  *
102
- * @group Audio
41
+ * Codes are added as something starts producing them, never in advance: a code
42
+ * nothing can emit tells a game to handle a case that cannot happen. Adding one
43
+ * is additive and safe.
103
44
  */
104
- declare const AUDIO_MASTER = "master";
105
45
  /**
106
- * How loud the game may be, as the host has it set right now.
46
+ * The codes a call fails with, as the keys of this interface.
107
47
  *
108
- * **Mute and the master fader are separate facts and stay separate.** A player
109
- * who mutes a game and then unmutes it expects the volume they had, not the
110
- * one they would have to find again — so mute is not `master: 0`, and a game
111
- * that stored it as one would hand the player back silence.
48
+ * An interface rather than a union so that a preview can add the codes only
49
+ * its calls fail with, as it adds its own namespace to the SDK: a game is
50
+ * named the codes of what it uses.
112
51
  *
113
- * The whole state travels every time rather than a delta, for the reason
114
- * `layout.change` carries the whole layout: these are small, they arrive
115
- * rarely, and a game that missed one — because it was loading, because a host
116
- * coalesced two — must not end up mixing from halves.
117
- *
118
- * @group Audio
52
+ * @group Bridge
119
53
  */
120
- type AudioState = {
54
+ interface ErrorCodes {
55
+ /** No handler on the other side. Usually means the host predates the method. */
56
+ unknown_method: true;
57
+ /** The handler rejected the shape of `params`. */
58
+ bad_params: true;
59
+ /** No reply inside the peer's timeout. Says nothing about whether it ran. */
60
+ timeout: true;
61
+ /** The peer was torn down with the call still in flight — a recycled slot. */
62
+ disposed: true;
121
63
  /**
122
- * Everything off, whatever the faders say.
64
+ * The call belonged to a session that has ended: the game started over inside
65
+ * a frame the host still holds. Distinct from `disposed` because the bridge is
66
+ * alive and the next call will work — it is this one that has nobody left to
67
+ * answer it.
68
+ */
69
+ stale: true;
70
+ /** The handshake asked for a protocol version this side cannot speak. */
71
+ unsupported_version: true;
72
+ /**
73
+ * This host does not do that at all — a site with no store answering
74
+ * `save.write`.
123
75
  *
124
- * The one control a host can count on a player reaching for in a hurry, and
125
- * the reason it is a boolean of its own: silence now, and the mix they had
126
- * when they come back.
76
+ * A different fact from `unknown_method`, which says the host is *older* than
77
+ * the method, and a game acts on the difference: one is a host that will
78
+ * never save and one is a host that would if it were newer. Neither is worth
79
+ * retrying, and both are worth saying honestly.
127
80
  */
128
- muted: boolean;
129
- /** The fader over all of it, `0`–`1`. */
130
- master: number;
81
+ unsupported: true;
82
+ /** Over this host's cap. Says trim, rather than go looking for a bug. */
83
+ too_large: true;
131
84
  /**
132
- * Each declared channel's own fader, `0`–`1`, by the id its manifest gave it.
85
+ * Faster than this host will take. The counterpart of the analytics refusal.
133
86
  *
134
- * A channel missing from here is `1` — a host that has never been asked about
135
- * `music` is not a host that wants it silent. That is what lets a game's next
136
- * version add a channel and have it audible for every player who already had
137
- * settings stored.
87
+ * Never what a host that *coalesced* two writes says: a write superseded by
88
+ * newer state from the same game has not been lost, and both promises
89
+ * resolve. This is for a host actually dropping writes, which usually means a
90
+ * game is saving from a render loop by mistake.
138
91
  */
139
- channels: Record<string, number>;
140
- };
141
- /**
142
- * What every game gets where nobody has said otherwise: audible, and mixed the
143
- * way the game meant it.
144
- *
145
- * The reading that claims the least, which for audio is the *loud* one — a game
146
- * that started silent against a host too old to have this API would be a game
147
- * that appeared broken to everybody, and a player who wants quiet has a control
148
- * for it on every host that has one.
149
- *
150
- * @group Audio
151
- */
152
- declare const AUDIO_FULL: AudioState;
153
- /**
154
- * How loud one channel actually comes out, which is the only number a game
155
- * needs and the one both sides must agree on.
156
- *
157
- * Here rather than in the game's SDK because the dev shell draws it too — a
158
- * developer reading *music: 0.35* off a panel and hearing something else has no
159
- * way to tell which half is wrong. Multiplication and not the smaller of the
160
- * two: a master fader is a fader over the mix, so halving it halves everything
161
- * under it rather than capping the loudest.
162
- *
163
- * @group Audio
164
- */
165
- declare function audioVolumeOf(state: AudioState, channel?: string): number;
166
- //#endregion
167
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/errors.d.ts
168
- /**
169
- * How a call fails.
170
- *
171
- * Requests reject; nothing throws synchronously once a peer exists. The codes
172
- * are the stable part — a game built against a newer SDK than the host it lands
173
- * on must be able to tell why it was refused and degrade, rather than take an
174
- * unhandled rejection into the player's face.
175
- *
176
- * Codes are added as something starts producing them, never in advance: a code
177
- * nothing can emit tells a game to handle a case that cannot happen. Adding one
178
- * is additive and safe.
179
- */
92
+ rate_limited: true;
93
+ /**
94
+ * Not now: what it needs can't be reached for the moment. Worth trying
95
+ * again shortly.
96
+ */
97
+ unavailable: true;
98
+ /** The handler threw. The message is ours, never the thrown value's. */
99
+ internal: true;
100
+ }
180
101
  /** @group Bridge */
181
- type ErrorCode =
182
- /** No handler on the other side. Usually means the host predates the method. */
183
- 'unknown_method' |
184
- /** The handler rejected the shape of `params`. */
185
- 'bad_params' |
186
- /** No reply inside the peer's timeout. Says nothing about whether it ran. */
187
- 'timeout' |
188
- /** The peer was torn down with the call still in flight — a recycled slot. */
189
- 'disposed' |
190
- /**
191
- * The call belonged to a session that has ended: the game started over inside
192
- * a frame the host still holds. Distinct from `disposed` because the bridge is
193
- * alive and the next call will work — it is this one that has nobody left to
194
- * answer it.
195
- */
196
- 'stale' |
197
- /** The handshake asked for a protocol version this side cannot speak. */
198
- 'unsupported_version' |
199
- /**
200
- * This host does not do that at all — a site with no store answering
201
- * `save.write`.
202
- *
203
- * A different fact from `unknown_method`, which says the host is *older* than
204
- * the method, and a game acts on the difference: one is a host that will
205
- * never save and one is a host that would if it were newer. Neither is worth
206
- * retrying, and both are worth saying honestly.
207
- */
208
- 'unsupported' |
209
- /** Over this host's cap. Says trim, rather than go looking for a bug. */
210
- 'too_large' |
211
- /**
212
- * Faster than this host will take. The counterpart of the analytics refusal.
213
- *
214
- * Never what a host that *coalesced* two writes says: a write superseded by
215
- * newer state from the same game has not been lost, and both promises
216
- * resolve. This is for a host actually dropping writes, which usually means a
217
- * game is saving from a render loop by mistake.
218
- */
219
- 'rate_limited' |
220
- /** The handler threw. The message is ours, never the thrown value's. */
221
- 'internal';
102
+ type ErrorCode = keyof ErrorCodes;
222
103
  /** @group Bridge */
223
104
  type ProtocolError = {
224
105
  code: ErrorCode;
@@ -234,48 +115,7 @@ declare class BridgeError extends Error {
234
115
  constructor(error: ProtocolError);
235
116
  }
236
117
  //#endregion
237
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/envelope.d.ts
238
- /**
239
- * `p` is the protocol version, carried on every message rather than only at the
240
- * handshake, because a message that arrives without context still has to be
241
- * classifiable.
242
- *
243
- * There is deliberately no addressing. A bridge joins exactly one host to
244
- * exactly one game: the game lives alone in its frame and has no way to name
245
- * another game, and the host holds one frame and needs no way to name another.
246
- * A host that runs several games at once — the app, the production site — runs
247
- * several bridges, each with its own transport, and keeping their inboxes apart
248
- * is that host's problem with its own frames. Putting a channel in the envelope
249
- * would make every transport pay for a problem only one kind of host has.
250
- *
251
- * @group Bridge
252
- */
253
- type Envelope = {
254
- p: 1;
255
- kind: 'req';
256
- id: string;
257
- method: string;
258
- params: unknown;
259
- } | {
260
- p: 1;
261
- kind: 'res';
262
- id: string;
263
- ok: true;
264
- value: unknown;
265
- } | {
266
- p: 1;
267
- kind: 'res';
268
- id: string;
269
- ok: false;
270
- error: ProtocolError;
271
- } | {
272
- p: 1;
273
- kind: 'evt';
274
- topic: string;
275
- data: unknown;
276
- };
277
- //#endregion
278
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/input.d.ts
118
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/input.d.ts
279
119
  /**
280
120
  * What the host says about how this game is being played.
281
121
  *
@@ -312,8 +152,6 @@ type Envelope = {
312
152
  * @group Input
313
153
  */
314
154
  type InputScheme = 'keyboard' | 'mouse' | 'touch' | 'gamepad';
315
- /** @group Input */
316
- declare const INPUT_SCHEMES: readonly InputScheme[];
317
155
  /**
318
156
  * What presses one action, and what to call it in front of a player.
319
157
  *
@@ -336,87 +174,8 @@ type InputBinding = {
336
174
  /** What to show for the binding a player would actually use. See above. */
337
175
  label?: string;
338
176
  };
339
- /**
340
- * How a group of on-screen buttons is arranged. See `ControlShape` in
341
- * `@hopple/manifest`, which a test keeps this in step with.
342
- *
343
- * @group Input
344
- */
345
- type InputLayoutShape = 'row' | 'column' | 'diamond' | 'cluster';
346
- /**
347
- * Where on the screen a group of on-screen controls sits. See `ControlZone` in
348
- * `@hopple/manifest`.
349
- *
350
- * @group Input
351
- */
352
- type InputLayoutZone = 'left' | 'right' | 'full';
353
- /**
354
- * The one direction an on-screen thumbstick moves in. See `ControlAxis` in
355
- * `@hopple/manifest`.
356
- *
357
- * @group Input
358
- */
359
- type InputLayoutAxis = 'x' | 'y';
360
- /**
361
- * How an on-screen thumbstick meets a thumb: a player's setting, the same in
362
- * every game they open.
363
- *
364
- * `floating` is marked where the layout puts it, as a sign that there is a
365
- * stick, and comes to wherever a thumb lands on its side of the screen — so a
366
- * player never has to find it, and the place they first touch is always the
367
- * centre. A thumb that goes past its rim pulls it along, so turning back is
368
- * never further than the stick is wide. `fixed` stays where it is drawn and is
369
- * moved only from there.
370
- *
371
- * @group Input
372
- */
373
- type InputSticks = 'floating' | 'fixed';
374
- /**
375
- * Why the mapping or the scheme is different from what the game was told before.
376
- *
377
- * @group Input
378
- */
379
- type InputChangeCause =
380
- /** A player changed a binding. */
381
- 'remap' |
382
- /** They picked up a controller, or put one down. */
383
- 'scheme' |
384
- /** The host revised it for its own reasons. */
385
- 'host' |
386
- /** A player changed how the on-screen controls behave. */
387
- 'setting';
388
- //#endregion
389
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/panel.d.ts
390
- /**
391
- * The pages of the panel this version of the protocol has a name for.
392
- *
393
- * **A vocabulary, not a capability.** Every one of these is a subject both ends
394
- * already have words for elsewhere — a game's own description, its boards, its
395
- * achievements, its bindings — so the names are the ones the hosts already use
396
- * for the pages they draw, and a game author who reads "About" in the panel
397
- * finds `about` here.
398
- *
399
- * It grows, and it only ever grows: a name means the same thing for ever,
400
- * because a game ships and is never updated. A host older than a name answers
401
- * `unsupported`, which is the same thing it says about a page it simply does
402
- * not draw — and the right answer to both, because from the game's side they
403
- * are one fact: that page is not going to open here.
404
- *
405
- * @group Panels
406
- */
407
- type PageId =
408
- /** What this game is: its title, its author, its description. */
409
- 'about' |
410
- /** Who has scored what, on this game's boards. */
411
- 'scores' |
412
- /** What has been unlocked, and how far along the rest is. */
413
- 'achievements' |
414
- /** What this game is played with, and where a player rebinds it. */
415
- 'input';
416
- /** @group Panels */
417
- declare const PAGES: readonly PageId[];
418
177
  //#endregion
419
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/pause.d.ts
178
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/pause.d.ts
420
179
  /**
421
180
  * Whether the game is holding still, which the host may ask for and only the
422
181
  * game can answer.
@@ -473,23 +232,8 @@ type PauseState =
473
232
  * that pressing it would do something.
474
233
  */
475
234
  'unpausable';
476
- /** @group Pause */
477
- declare const PAUSE_STATES: readonly PauseState[];
478
- /**
479
- * The cautious reading, and the one every silence falls back to: a game that
480
- * has said nothing cannot be paused.
481
- *
482
- * The opposite of the reading `AUDIO_FULL` takes, and for the same reason —
483
- * both are the answer that claims the least. Guessing loud is safe for volume
484
- * because a game that started silent would look broken; guessing `unpausable`
485
- * is safe here because a host that drew Pause on a game with no pause would
486
- * have promised a button that does nothing.
487
- *
488
- * @group Pause
489
- */
490
- declare const PAUSE_NONE: PauseState;
491
235
  //#endregion
492
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/save.d.ts
236
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/save.d.ts
493
237
  /**
494
238
  * The one thing a game may keep.
495
239
  *
@@ -540,45 +284,7 @@ type SaveStored = {
540
284
  bytes: number;
541
285
  };
542
286
  //#endregion
543
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/score.d.ts
544
- /**
545
- * What a player did, as one number.
546
- *
547
- * A game submits a score when a run ends; the host makes it durable and, if it
548
- * has anywhere to send it, sends it. What comes back is what the player is
549
- * shown: their own best, and where it stands.
550
- *
551
- * **The shape of this is not the shape of a save, and not the shape of
552
- * analytics.** A save is one document rewritten in place, and losing one costs
553
- * a player their progress. A score is an append — every run is its own fact, a
554
- * later worse run does not erase a better one, and the collapsing into "your
555
- * best" happens where the rows are rather than on the way in. Analytics is
556
- * one-way because nothing waits for it; a score is a request because a game
557
- * draws the answer.
558
- *
559
- * **The submission carries no player.** The game says which board and what
560
- * number, and the host attaches who — which is the host being authoritative
561
- * about the one fact a game must not be able to assert. It also means a game
562
- * literally cannot submit on somebody else's behalf, which is free here and
563
- * would be expensive to retrofit.
564
- *
565
- * **There is no anti-cheat and this file does not pretend otherwise.** A score
566
- * is whatever the machine running the game says it is. What is checked is that
567
- * the number is a number: finite, and inside the range where an integer still
568
- * round-trips. The realistic thing that catches is an uninitialised variable.
569
- */
570
- /**
571
- * The largest score the wire will carry, and the smallest.
572
- *
573
- * Spelled here *and* in `@hopple/manifest`, on purpose and with a test to keep
574
- * them equal. The server checks the same rule and is not allowed to depend on
575
- * this package — "the server never talks to a game", made mechanical — so the
576
- * rule is written down in the one package both sides may import and enforced on
577
- * the wire here.
578
- *
579
- * @group Score
580
- */
581
- declare const SCORE_VALUE_MAX = 9007199254740991;
287
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/score.d.ts
582
288
  /**
583
289
  * What the host says came of it.
584
290
  *
@@ -616,7 +322,7 @@ type ScoreStanding = {
616
322
  of?: number;
617
323
  };
618
324
  //#endregion
619
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/share.d.ts
325
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/share.d.ts
620
326
  /**
621
327
  * A link to this game, offered to the player by the host.
622
328
  *
@@ -715,7 +421,7 @@ type ShareMeta = {
715
421
  [key: string]: ShareMetaValue;
716
422
  };
717
423
  //#endregion
718
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/methods.d.ts
424
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/methods.d.ts
719
425
  /**
720
426
  * How far a game has got with its own preparation.
721
427
  *
@@ -895,7 +601,7 @@ type DeviceInfo = {
895
601
  tier: DeviceTier;
896
602
  };
897
603
  //#endregion
898
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/listeners.d.ts
604
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/listeners.d.ts
899
605
  /**
900
606
  * The one way anything here is listened to: `on(listener)` hands back the
901
607
  * function that stops it.
@@ -924,152 +630,17 @@ type DeviceInfo = {
924
630
  */
925
631
  type Unsubscribe = () => void;
926
632
  //#endregion
927
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/protocol/src/transport.d.ts
928
- /** @group Bridge */
929
- type Transport = {
930
- send(message: Envelope): void;
931
- /** Returns the unsubscribe. Calling `receive` twice is not defined behaviour. */
932
- receive(handler: (message: Envelope) => void): () => void;
933
- close(): void;
934
- };
935
- /**
936
- * The name a platform leaves its shim under, and the name it calls if it is late.
937
- *
938
- * Declared here, in the package both ends import, so that a platform author and
939
- * the SDK are reading the same two strings. Nothing in this package writes
940
- * either of them: the shim is wholly the platform's, and this file is a target
941
- * to hit rather than code we ship.
942
- *
943
- * @group Bridge
944
- */
945
- declare const SHIM = "__HOPPLE_SHIM__";
946
- /** @group Bridge */
947
- declare const SET_SHIM = "__HOPPLE_SET_SHIM__";
948
- /**
949
- * What a native shell leaves for a game to talk through.
950
- *
951
- * A game in a browser frame has `window.parent` and needs none of this. A game
952
- * in a WebView is a top-level document — its `parent` is itself — so there is no
953
- * window above it to post to, and the platform has to hand it a channel instead.
954
- * This is that channel, reduced to the least a platform can be asked for.
955
- *
956
- * **The SDK writes none of it.** It agrees on a name and a shape and finds
957
- * whatever was left; how the bytes actually cross is the platform's business,
958
- * and is different on every one. That is what keeps this package free of hosts
959
- * it has never heard of.
960
- *
961
- * A shim has exactly two duties:
962
- *
963
- * - **Arrive.** Before the game's bundle runs is easiest, and is what our own
964
- * hosts do — a document-start hook, `injectedJavaScriptBeforeContentLoaded`,
965
- * a `WKUserScript` at `.atDocumentStart`. A platform that cannot manage that
966
- * is late rather than broken: see the rendezvous below.
967
- * - **Never throw.** A shim that throws breaks a game that did nothing wrong.
968
- *
969
- * And one duty it explicitly does *not* have: **it must not buffer.** Nothing
970
- * arrives before the game is listening, because every host → game message is
971
- * gated on the handshake and the game is the side that opens it. An array in
972
- * every shim would be paying, on every platform, for a message that is never
973
- * sent.
974
- *
975
- * **Two ways in, and a platform may use either**, because neither side may
976
- * assume it is first:
977
- *
978
- * ```js
979
- * // The whole of what a platform has to get right about ordering.
980
- * if (w.__HOPPLE_SET_SHIM__) w.__HOPPLE_SET_SHIM__(shim);
981
- * else w.__HOPPLE_SHIM__ = shim;
982
- * ```
983
- *
984
- * A platform that installs early always takes the second branch; one that
985
- * cannot takes the first, and the SDK — which has been waiting — connects. The
986
- * setter only exists while the SDK is waiting, and is removed once it is used
987
- * or the wait expires.
988
- *
989
- * **Strings, not envelopes.** Every native bridge carries strings, so that is
990
- * the contract, and it means the SDK stays the trust boundary: `JSON.parse` and
991
- * `isEnvelope` run in SDK code on both sides of the wire and never in a
992
- * platform's. A shim cannot get validation wrong, because it does none.
993
- *
994
- * @group Bridge
995
- */
996
- type HoppleShim = {
997
- /**
998
- * The contract version. `1` today.
999
- *
1000
- * A shim whose version the SDK does not know is ignored rather than guessed
1001
- * at — which falls through to the frame branch, and then to the wait.
1002
- */
1003
- v: 1;
1004
- /** Send one already-serialised envelope to the host. Game → host. */
1005
- send(message: string): void;
1006
- /**
1007
- * Take the SDK's inbound handler, and hand back the unsubscribe.
1008
- *
1009
- * Called once, by the SDK, and it is a subscription rather than a delivery:
1010
- * how the platform later reaches the handler it was given — an eval into a
1011
- * global, a real message channel — is the platform's problem and is not
1012
- * described here.
1013
- */
1014
- receive(handler: (message: string) => void): () => void;
1015
- };
1016
- /**
1017
- * The two globals, so a platform's own TypeScript sees the same shape the SDK
1018
- * reads. Neither is ever written by this package.
1019
- */
1020
- declare global {
1021
- var __HOPPLE_SHIM__: HoppleShim | undefined;
1022
- var __HOPPLE_SET_SHIM__: ((shim: HoppleShim) => void) | undefined;
1023
- }
1024
- //#endregion
1025
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/manifest/src/manifest.d.ts
1026
- /**
1027
- * What a game says about itself.
1028
- *
1029
- * Written in the project's hopple config and uploaded beside the game's page
1030
- * as `game.manifest.json`. It is the first thing the host learns: it is
1031
- * published with the game, so a host holds it before it has anything to run —
1032
- * from the server in the app, from the project in the dev server. It never
1033
- * travels over the bridge. Not to be confused with `FeedEntry`, which is
1034
- * what the *catalogue* says about a game.
1035
- *
1036
- * The core is deliberately tiny: identity, a name, and how the game wants to
1037
- * be held. Everything a later API needs declared ahead of time
1038
- * gets its own optional top-level key named after that API — `save`,
1039
- * `achievements`, and so on — whose contents are designed by the milestone that
1040
- * builds it. Most namespaces will never need one.
1041
- *
1042
- * **The types below are the only place a rule is written.** Their shapes and
1043
- * the constraint tags in their comments — `@maxLength`, `@pattern` and the rest
1044
- * — are compiled into `game.manifest.schema.json`, and `validateManifest` checks
1045
- * a manifest by walking that schema. Change a type and the schema, the
1046
- * validator and every editor that reads the schema follow. The only rules
1047
- * written in code are the few no schema can state, and they are at the bottom
1048
- * of this file.
1049
- *
1050
- * Validation lives here rather than in the host so that the publish CLI, the
1051
- * dev server and the host all reach the same verdict about the same file.
1052
- *
1053
- * @module manifest
1054
- */
1055
- /**
1056
- * The name a manifest is uploaded under, beside the game's page.
1057
- *
1058
- * @group Manifest
1059
- * @category Useful
1060
- */
1061
- declare const MANIFEST_FILE = "game.manifest.json";
633
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/manifest/src/manifest.d.ts
1062
634
  /**
1063
- * The manifest's own version, separate from the protocol's.
1064
- *
1065
- * Bumped only if the *core* changes meaning, which is a thing we intend never
1066
- * to do — new keys are additive and need no bump. A host refuses a schema it
1067
- * does not know, because a manifest it half-understands is worse than none.
635
+ * The API a game is written against: `M.m`, a major and the least minor it
636
+ * needs, or `next`, the pre-releases of the next stable release.
1068
637
  *
1069
638
  * @group Manifest
1070
- * @category Useful
639
+ * @category Important
640
+ * @pattern ^(?:(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)|next)$
641
+ * @errorMessage must be the API the game is written against, like "1.0", or "next"
1071
642
  */
1072
- declare const MANIFEST_SCHEMA = 1;
643
+ type ApiVersion = string;
1073
644
  /**
1074
645
  * How the game wants to be held. The host decides what to do about it.
1075
646
  *
@@ -1100,7 +671,6 @@ type InsetMode =
1100
671
  */
1101
672
  type LayoutDeclaration = {
1102
673
  insets: InsetMode;
1103
- [key: string]: unknown;
1104
674
  };
1105
675
  /**
1106
676
  * What a prop may hold, as one word. The three the wire carries, and no more.
@@ -1152,7 +722,6 @@ type AnalyticsDeclaration = {
1152
722
  * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, not: { pattern: '^hopple\\.' }, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores, and must not start with "hopple.", which the host uses' }
1153
723
  */
1154
724
  events: Record<string, AnalyticsShape>;
1155
- [key: string]: unknown;
1156
725
  };
1157
726
  /**
1158
727
  * What a game expects to keep, in bytes.
@@ -1182,7 +751,6 @@ type SaveDeclaration = {
1182
751
  * @maximum 1048576
1183
752
  */
1184
753
  bytes: number;
1185
- [key: string]: unknown;
1186
754
  };
1187
755
  /**
1188
756
  * One thing a game makes noise with, as the player is offered it.
@@ -1204,7 +772,6 @@ type AudioChannel = {
1204
772
  * @errorMessage must not be blank
1205
773
  */
1206
774
  title: string;
1207
- [key: string]: unknown;
1208
775
  };
1209
776
  /**
1210
777
  * What this game makes noise with, so a host can offer a fader for each.
@@ -1245,7 +812,6 @@ type AudioDeclaration = {
1245
812
  * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, not: { const: 'master' }, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores, and must not be "master", which every host adds itself' }
1246
813
  */
1247
814
  channels?: Record<string, AudioChannel>;
1248
- [key: string]: unknown;
1249
815
  };
1250
816
  /**
1251
817
  * Which way a board sorts — the one thing about a score that cannot be guessed.
@@ -1283,7 +849,6 @@ type ScoreBoard = {
1283
849
  direction: ScoreDirection;
1284
850
  /** See `ScoreFormat`. Absent means `integer`. */
1285
851
  format?: ScoreFormat;
1286
- [key: string]: unknown;
1287
852
  };
1288
853
  /**
1289
854
  * The boards this game keeps a score on.
@@ -1314,7 +879,6 @@ type ScoreDeclaration = {
1314
879
  * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
1315
880
  */
1316
881
  boards: Record<string, ScoreBoard>;
1317
- [key: string]: unknown;
1318
882
  };
1319
883
  /**
1320
884
  * One named step on the way to a measured achievement's target.
@@ -1353,7 +917,6 @@ type AchievementTier = {
1353
917
  * @errorMessage must not be blank
1354
918
  */
1355
919
  icon?: string;
1356
- [key: string]: unknown;
1357
920
  };
1358
921
  /**
1359
922
  * What makes an achievement measured rather than flagged.
@@ -1399,7 +962,6 @@ type AchievementProgress = {
1399
962
  * @maxItems 8
1400
963
  */
1401
964
  tiers?: AchievementTier[];
1402
- [key: string]: unknown;
1403
965
  };
1404
966
  /**
1405
967
  * One thing a player can be shown to have done.
@@ -1450,7 +1012,6 @@ type Achievement = {
1450
1012
  secret?: boolean;
1451
1013
  /** Present makes this measured rather than flagged. See `AchievementProgress`. */
1452
1014
  progress?: AchievementProgress;
1453
- [key: string]: unknown;
1454
1015
  };
1455
1016
  /**
1456
1017
  * The achievements this game keeps for a player.
@@ -1479,7 +1040,6 @@ type AchievementDeclaration = {
1479
1040
  * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
1480
1041
  */
1481
1042
  list: Record<string, Achievement>;
1482
- [key: string]: unknown;
1483
1043
  };
1484
1044
  /**
1485
1045
  * A device a game can be played with, as one word.
@@ -1577,7 +1137,115 @@ type InputAction = {
1577
1137
  * @errorMessage must not be blank
1578
1138
  */
1579
1139
  label?: string;
1580
- [key: string]: unknown;
1140
+ };
1141
+ /**
1142
+ * What presses a player's action on a pad or on the screen: a pad button, or
1143
+ * an on-screen control. Keys and mouse buttons are a keyboard set's, in
1144
+ * `PlayerInputDeclaration.keyboard`.
1145
+ *
1146
+ * @group Input
1147
+ * @pattern ^(?:controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.(?:north|south|east|west|start|select)|pad\.(?:left|right)\.(?:north|south|east|west|bumper|trigger))$
1148
+ * @errorMessage must be a pad button or an on-screen control: a player's keys and mouse buttons go in input.player.keyboard
1149
+ */
1150
+ type PlayerBinding = string;
1151
+ /**
1152
+ * What presses a player's action on their part of the keyboard: a key, or a
1153
+ * mouse button.
1154
+ *
1155
+ * @group Input
1156
+ * @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\.(?:left|middle|right))$
1157
+ * @errorMessage must be a key or a mouse button (and Escape is the host's key)
1158
+ */
1159
+ type KeyboardBinding = string;
1160
+ /**
1161
+ * One thing each player does: the same action for every local player, pressed
1162
+ * by their own pad, their own part of the keyboard, or the on-screen controls.
1163
+ *
1164
+ * @group Input
1165
+ */
1166
+ type PlayerInputAction = {
1167
+ /**
1168
+ * What presses it on a pad, the same for every pad, and on the screen.
1169
+ * Leave it out for an action pressed only on the keyboard.
1170
+ *
1171
+ * @minItems 1
1172
+ * @maxItems 8
1173
+ */
1174
+ bind?: PlayerBinding[];
1175
+ /**
1176
+ * What to call it in front of a player, in a remapping screen.
1177
+ *
1178
+ * @pattern \S
1179
+ * @maxLength 40
1180
+ * @errorMessage must not be blank
1181
+ */
1182
+ title?: string;
1183
+ /**
1184
+ * What to draw on an on-screen button: a glyph, or a letter.
1185
+ *
1186
+ * @pattern \S
1187
+ * @maxLength 8
1188
+ * @errorMessage must not be blank
1189
+ */
1190
+ label?: string;
1191
+ /**
1192
+ * Whether pressing it on a pad or a part of the keyboard nobody plays with
1193
+ * makes a new player, while the game is taking players. Left out on every
1194
+ * action, any of them does.
1195
+ */
1196
+ join?: boolean;
1197
+ };
1198
+ /**
1199
+ * One part of the keyboard one player plays with: the keys and mouse buttons
1200
+ * that press each of the player actions there.
1201
+ *
1202
+ * @group Input
1203
+ */
1204
+ type KeyboardSet = {
1205
+ /**
1206
+ * What the players call it: *Left keys*, *Arrows*.
1207
+ *
1208
+ * @pattern \S
1209
+ * @maxLength 40
1210
+ * @errorMessage must not be blank
1211
+ */
1212
+ title?: string;
1213
+ /**
1214
+ * The keys and mouse buttons of each player action, by its id.
1215
+ *
1216
+ * @maxProperties 64
1217
+ * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a player action id' }
1218
+ */
1219
+ bind: Record<string, KeyboardBinding[]>;
1220
+ };
1221
+ /**
1222
+ * What each local player does, and how they share the keyboard.
1223
+ *
1224
+ * **One set of actions for every player.** Each local player has their own
1225
+ * copy, pressed by the pad they hold, the part of the keyboard they play with,
1226
+ * or the on-screen controls. A pad's buttons are the same for every pad, so any
1227
+ * number of pads just works; the keyboard is split into sets, one player each,
1228
+ * so the number of sets is how many can share it.
1229
+ *
1230
+ * @group Input
1231
+ */
1232
+ type PlayerInputDeclaration = {
1233
+ /**
1234
+ * The actions each player has, by id: never an id `actions` declares too.
1235
+ *
1236
+ * @maxProperties 32
1237
+ * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
1238
+ */
1239
+ actions: Record<string, PlayerInputAction>;
1240
+ /**
1241
+ * The parts of the keyboard players play with, one player each, in the
1242
+ * order they are offered. A player alone hears every one of them. Left out,
1243
+ * the keyboard presses no player's action.
1244
+ *
1245
+ * @minItems 1
1246
+ * @maxItems 4
1247
+ */
1248
+ keyboard?: KeyboardSet[];
1581
1249
  };
1582
1250
  /**
1583
1251
  * Where on the screen a group of on-screen controls sits.
@@ -1641,7 +1309,6 @@ type ControlSlot = {
1641
1309
  as?: InputStick | Record<string, InputBinding$1>;
1642
1310
  /** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
1643
1311
  axis?: ControlAxis;
1644
- [key: string]: unknown;
1645
1312
  };
1646
1313
  /**
1647
1314
  * The one direction an on-screen thumbstick moves in: `x` across, `y` up and
@@ -1674,8 +1341,8 @@ type ControlZoneSlots = Record<string, ControlSlot>;
1674
1341
  * A game may declare several and switch between them — a menu wants different
1675
1342
  * controls from play, and a driving section different ones again — which is why
1676
1343
  * they are named rather than there being one. A zone nobody knows is a layout
1677
- * from a newer SDK, which the additive rule says must still load, so whoever
1678
- * draws it simply has nowhere to put it.
1344
+ * from a newer minor, which a host reads leniently, so whoever draws it simply
1345
+ * has nowhere to put it.
1679
1346
  *
1680
1347
  * @group Input
1681
1348
  */
@@ -1683,7 +1350,6 @@ type ControlLayout = {
1683
1350
  left?: ControlZoneSlots;
1684
1351
  right?: ControlZoneSlots;
1685
1352
  full?: ControlZoneSlots;
1686
- [key: string]: unknown;
1687
1353
  };
1688
1354
  /**
1689
1355
  * What a game says about how it is played.
@@ -1733,20 +1399,82 @@ type InputDeclaration = {
1733
1399
  * @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a layout name of lowercase letters, digits, dots, hyphens and underscores' }
1734
1400
  */
1735
1401
  controls?: Record<string, ControlLayout>;
1736
- [key: string]: unknown;
1402
+ /**
1403
+ * What each local player does, beside `actions`, which are everybody's. See
1404
+ * `PlayerInputDeclaration`.
1405
+ */
1406
+ player?: PlayerInputDeclaration;
1737
1407
  };
1738
1408
  /**
1739
- * The index signature is the additive-only rule applied to this file: a game
1740
- * built against a newer SDK declares keys an older host has never heard of, and
1741
- * must still run there. It costs a typo in a manifest literal going unnoticed
1742
- * by the compiler, which the validator catches instead.
1409
+ * How many people play a game on one device.
1410
+ *
1411
+ * Read by hosts before the game runs: a listing says *1–4 on one device*, and
1412
+ * the input panel offers a row for each player.
1413
+ *
1414
+ * @group Input
1415
+ * @category Useful
1416
+ */
1417
+ type PlayersDeclaration = {
1418
+ /**
1419
+ * The most players on one device, each with their own copy of
1420
+ * `input.player`'s actions. 1 when left out.
1421
+ *
1422
+ * @minimum 1
1423
+ * @maximum 8
1424
+ * @asType integer
1425
+ */
1426
+ local?: number;
1427
+ };
1428
+ /**
1429
+ * A preview's revision: a whole number from 1, the `R` of its package's
1430
+ * `0.R.p`. A breaking change to the preview is a new revision.
1431
+ *
1432
+ * @group Manifest
1433
+ * @category Useful
1434
+ * @minimum 1
1435
+ * @asType integer
1436
+ */
1437
+ type PreviewRevision = number;
1438
+ /**
1439
+ * The previews a game is written against, each at the revision it needs.
1440
+ *
1441
+ * A preview is a part of the SDK that is not stable yet: its own `0.x`
1442
+ * package, versioned apart from `api`, and served only to the games that
1443
+ * declare it, at the revision they declare. A change to a preview reaches the
1444
+ * games written against it and nobody else.
1445
+ *
1446
+ * @group Manifest
1447
+ * @category Useful
1448
+ */
1449
+ type PreviewsDeclaration = {
1450
+ /**
1451
+ * That this game plays over the network, in matches, at this revision of
1452
+ * `@hopple/sdk-preview-network`: `sdk.match`.
1453
+ *
1454
+ * The permission a reviewer grants, and what lets a host offer *Play with
1455
+ * friends* before the game has loaded. Nothing else about networking is
1456
+ * declared: the way a match is played, and its size, are chosen by the game
1457
+ * when it makes one. Absent, a game is never told about a match, and never
1458
+ * reaches one.
1459
+ */
1460
+ network?: PreviewRevision;
1461
+ };
1462
+ /**
1463
+ * Publishing refuses a key the manifest's API does not know; a host reading a
1464
+ * published one ignores it (see `validateManifest`).
1743
1465
  *
1744
1466
  * @group Manifest
1745
1467
  * @category Important
1746
1468
  */
1747
1469
  type GameManifest = {
1748
- /** @category Important */
1749
- schema: typeof MANIFEST_SCHEMA;
1470
+ /**
1471
+ * The API the game is written against. The major picks what the game is
1472
+ * served; the minor is the least it needs. The CLI fills it from the
1473
+ * installed `@hopple/sdk` when the config leaves it out.
1474
+ *
1475
+ * @category Important
1476
+ */
1477
+ api: ApiVersion;
1750
1478
  /**
1751
1479
  * Stable across versions, and unique among its publisher's games: it is the
1752
1480
  * second half of the address the game is published at, `owner/id`, and the
@@ -1811,22 +1539,6 @@ type GameManifest = {
1811
1539
  cover?: string;
1812
1540
  /** @category Important */
1813
1541
  orientation: Orientation;
1814
- /**
1815
- * The `@hopple/sdk` version the game was built against, optionally prefixed
1816
- * with `^` or `~`.
1817
- *
1818
- * Recorded rather than enforced: what the host and the game actually have to
1819
- * agree on is the protocol version, which the handshake settles. This is for
1820
- * diagnostics — "every game that broke was built against 2.3" is a question
1821
- * worth being able to ask. Deliberately far narrower than everything npm
1822
- * accepts: a grammar we can read in one line is a grammar three packages can
1823
- * agree on.
1824
- *
1825
- * @category Important
1826
- * @pattern ^[\^~]?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
1827
- * @errorMessage must be a version like 1.0.0, ^1.0.0 or ~1.0.0
1828
- */
1829
- sdk: string;
1830
1542
  /**
1831
1543
  * How the game wants the host's furniture handled. See `InsetMode`.
1832
1544
  *
@@ -1863,6 +1575,12 @@ type GameManifest = {
1863
1575
  * @category Useful
1864
1576
  */
1865
1577
  input?: InputDeclaration;
1578
+ /**
1579
+ * How many play on one device. See `PlayersDeclaration`.
1580
+ *
1581
+ * @category Useful
1582
+ */
1583
+ players?: PlayersDeclaration;
1866
1584
  /**
1867
1585
  * What this game makes noise with, one channel at a time, or `null` for a
1868
1586
  * game that makes none. See `AudioDeclaration`.
@@ -1870,7 +1588,13 @@ type GameManifest = {
1870
1588
  * @category Important
1871
1589
  */
1872
1590
  audio: AudioDeclaration | null;
1873
- [key: string]: unknown;
1591
+ /**
1592
+ * The previews this game is written against, each at its revision. See
1593
+ * `PreviewsDeclaration`.
1594
+ *
1595
+ * @category Useful
1596
+ */
1597
+ previews?: PreviewsDeclaration;
1874
1598
  };
1875
1599
  /**
1876
1600
  * A game project's hopple config: `hopple.config.ts`, `hopple.config.js` or
@@ -1888,7 +1612,7 @@ type GameManifest = {
1888
1612
  * export default {
1889
1613
  * build: 'npm run build',
1890
1614
  * page: 'dist/index.html',
1891
- * manifest: { schema: 1, id: 'my-game', version: '0.1.0', title: 'My Game', orientation: 'portrait', sdk: '^0.1.0', audio: null },
1615
+ * manifest: { id: 'my-game', version: '0.1.0', title: 'My Game', orientation: 'portrait', audio: null },
1892
1616
  * } satisfies HoppleConfig;
1893
1617
  * ```
1894
1618
  *
@@ -1908,18 +1632,29 @@ type HoppleConfig = {
1908
1632
  */
1909
1633
  build?: string;
1910
1634
  /**
1911
- * The built page, relative to the config: the one file that is published,
1912
- * so it has to hold the whole game.
1635
+ * The built page, relative to the config. The directory it is in is
1636
+ * published, with the page as its `index.html`; a page beside its config is
1637
+ * published alone, so it has to hold the whole game.
1913
1638
  *
1914
1639
  * @pattern \.[Hh][Tt][Mm][Ll]?$
1915
1640
  * @errorMessage must be the built .html file, like "dist/index.html"
1916
1641
  */
1917
1642
  page: string;
1918
1643
  /** What the game is. Uploaded beside the page, and read by every host before the game runs. */
1919
- manifest: GameManifest;
1644
+ manifest: ProjectManifest;
1645
+ };
1646
+ /**
1647
+ * The manifest as a project's config holds it: `api` may be left out, for the
1648
+ * CLI to fill from the installed `@hopple/sdk`.
1649
+ *
1650
+ * @group Manifest
1651
+ * @category Useful
1652
+ */
1653
+ type ProjectManifest = Omit<GameManifest, 'api'> & {
1654
+ api?: ApiVersion;
1920
1655
  };
1921
1656
  //#endregion
1922
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/achievements.d.ts
1657
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/achievements.d.ts
1923
1658
  /**
1924
1659
  * Where this player stands on one achievement.
1925
1660
  *
@@ -2017,7 +1752,7 @@ type Achievements<Id extends string = string, Measured extends string = Id> = {
2017
1752
  progress(id: Measured, value: number): void;
2018
1753
  };
2019
1754
  //#endregion
2020
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/analytics.d.ts
1755
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/analytics.d.ts
2021
1756
  /**
2022
1757
  * @group Analytics
2023
1758
  * @category Important
@@ -2034,7 +1769,7 @@ type Analytics<Events extends object = Record<string, AnalyticsProps>> = {
2034
1769
  track<Event extends keyof Events & string>(name: Event, props?: Events[Event]): void;
2035
1770
  };
2036
1771
  //#endregion
2037
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/audio.d.ts
1772
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/audio.d.ts
2038
1773
  /**
2039
1774
  * Something about the game's sound changed: the host's order, the player's
2040
1775
  * mute, a fader. Read what you need — `volume(channel)`, `allowed` — and apply
@@ -2066,8 +1801,6 @@ type Audio<Channel extends string = string> = {
2066
1801
  readonly muted: boolean;
2067
1802
  /** The fader over the whole game, `0`–`1`, ignoring mute. */
2068
1803
  readonly master: number;
2069
- /** The whole of what the host last said. Frozen; a new object on every change. */
2070
- readonly state: AudioState;
2071
1804
  /**
2072
1805
  * How loud one of this game's channels should actually come out, `0`–`1`.
2073
1806
  *
@@ -2127,7 +1860,7 @@ type Audio<Channel extends string = string> = {
2127
1860
  media(element: HTMLMediaElement, channel?: Channel): Unsubscribe;
2128
1861
  };
2129
1862
  //#endregion
2130
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/info.d.ts
1863
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/info.d.ts
2131
1864
  /**
2132
1865
  * What the host said, frozen.
2133
1866
  *
@@ -2145,7 +1878,7 @@ type Info = Readonly<{
2145
1878
  device: Readonly<DeviceInfo>;
2146
1879
  }>;
2147
1880
  //#endregion
2148
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/layout.d.ts
1881
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/layout.d.ts
2149
1882
  /**
2150
1883
  * How much of each edge belongs to the host. CSS pixels.
2151
1884
  *
@@ -2255,7 +1988,7 @@ type InsetPolicy = {
2255
1988
  */
2256
1989
  declare function insetsFrom(obstructions: readonly Obstruction[], frame: Size, policy?: InsetPolicy): Insets;
2257
1990
  //#endregion
2258
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/names.d.ts
1991
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/names.d.ts
2259
1992
  /**
2260
1993
  * The keys of a declared map, or `string` where the map is not a literal one —
2261
1994
  * which is what a manifest typed as the wide `GameManifest` gives — and
@@ -2274,6 +2007,19 @@ type ActionName<M> = M extends {
2274
2007
  actions?: infer A;
2275
2008
  };
2276
2009
  } ? DeclaredNames<NonNullable<A>> : never;
2010
+ /**
2011
+ * An action each local player has, as the manifest declares it under
2012
+ * `input.player.actions`.
2013
+ *
2014
+ * @group Names
2015
+ */
2016
+ type PlayerActionName<M> = M extends {
2017
+ input?: {
2018
+ player?: {
2019
+ actions?: infer A;
2020
+ };
2021
+ };
2022
+ } ? DeclaredNames<NonNullable<A>> : never;
2277
2023
  /**
2278
2024
  * An on-screen control layout the manifest declares under `input.controls`.
2279
2025
  *
@@ -2362,7 +2108,7 @@ type EventProps<M> = M extends {
2362
2108
  */
2363
2109
  type StickName = 'pad.left.stick' | 'pad.right.stick' | 'pad.stick' | `controls.${string}`;
2364
2110
  //#endregion
2365
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/state.d.ts
2111
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/state.d.ts
2366
2112
  /**
2367
2113
  * Where the pointer is, in the frame's own coordinates.
2368
2114
  *
@@ -2423,246 +2169,36 @@ type InputSnapshot<Action extends string = string> = {
2423
2169
  pointer: PointerState | null;
2424
2170
  };
2425
2171
  //#endregion
2426
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/controls.d.ts
2427
- /**
2428
- * The name a host leaves its input shim under.
2429
- *
2430
- * Beside `SHIM` in spirit and separate from it in fact: that one is a channel a
2431
- * platform must provide for a game to speak at all, and this one is an opinion
2432
- * about drawing that most hosts will not have. A game with no shim draws the
2433
- * default controls and is completely playable — which is what an embed on
2434
- * somebody else's page gets, since there is nowhere for anyone to inject one.
2435
- *
2436
- * @group Input
2437
- */
2438
- declare const INPUT_SHIM = "__HOPPLE_INPUT__";
2439
- /**
2440
- * One control, as a renderer is given it. Resolved: no ids left to look up.
2441
- *
2442
- * @group Input
2443
- */
2444
- type ControlModelSlot = {
2445
- id: string;
2446
- type: 'buttons' | 'thumbstick';
2447
- /** Always a shape this SDK knows. An unfamiliar one has already become `column`. */
2448
- shape: InputLayoutShape;
2449
- /**
2450
- * For `buttons`: what each one presses, what to draw on it, and the standard
2451
- * control it stands in for where the layout said one.
2452
- */
2453
- buttons: readonly {
2454
- action: string;
2455
- label: string;
2456
- as?: string;
2457
- }[];
2458
- /** For `thumbstick`: the stick id it drives. */
2459
- stick: string;
2460
- /**
2461
- * For `thumbstick`: the one axis it moves along, where the layout held it to
2462
- * one. Absent, it moves both ways. A renderer that draws it freely anyway is
2463
- * still correct: the SDK zeroes the other axis of whatever is reported.
2464
- */
2465
- axis?: InputLayoutAxis;
2466
- };
2467
- /**
2468
- * @group Input
2469
- */
2470
- type ControlModelZone = {
2471
- zone: InputLayoutZone;
2472
- controls: readonly ControlModelSlot[];
2473
- };
2474
- /**
2475
- * A whole layout, resolved against the manifest's actions.
2476
- *
2477
- * @group Input
2478
- */
2479
- type ControlModel = {
2480
- /** Which named layout this is. */
2481
- layout: string;
2482
- zones: readonly ControlModelZone[];
2483
- /** The frame the controls are being drawn on, when it is known. */
2484
- frame: Size;
2485
- /**
2486
- * How much of that frame the host has taken along each edge.
2487
- *
2488
- * The host's furniture only — its rail, its corner control, the device's
2489
- * notch. Emphatically *not* the pad's own rectangles, which reach a game as
2490
- * obstructions of kind `controls` and would, if they were projected in here,
2491
- * inset the pad by the pad on every pass until it walked off the frame.
2492
- * Whoever fills this in owes that filter; `index.ts` pays it.
2493
- *
2494
- * Zero is the ordinary answer and always a safe one: a host that reserved the
2495
- * frame instead of drawing over it has already shrunk `frame` clear of its
2496
- * own furniture, and a host too old to say anything has said nothing. Both
2497
- * land here as four zeroes and the pad sits where it always sat.
2498
- */
2499
- insets: Insets;
2500
- /**
2501
- * How a thumbstick meets a thumb, as the player set it on their host.
2502
- *
2503
- * `floating`: marked where the layout puts it, so a player can see there is
2504
- * a stick, brought to wherever a thumb lands on its side of the screen, so
2505
- * the place they first touch is the centre, and pulled along by a thumb that
2506
- * goes past its rim. `fixed`: moved only from where it is drawn. A renderer
2507
- * that ignores this draws `fixed`, which is still a stick a game can be
2508
- * played with.
2509
- */
2510
- sticks: InputSticks;
2511
- };
2512
- /**
2513
- * How a renderer says something happened.
2514
- *
2515
- * The only way out of a renderer, replaced or not. A shim reports and the SDK
2516
- * decides: that is what keeps a replaced renderer from being able to leave an
2517
- * action held forever.
2518
- *
2519
- * @group Input
2520
- */
2521
- type ControlReport = {
2522
- /** `index` is into the control's own `buttons`. */
2523
- press(control: string, index: number, down: boolean): void;
2524
- /** A stick's position, each axis in −1..1 and already inside the unit circle. */
2525
- move(control: string, x: number, y: number): void;
2526
- };
2172
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/players.d.ts
2527
2173
  /**
2528
- * What a host may install to change how controls are drawn.
2529
- *
2530
- * **Every hook is optional and every one is wrapped.** A shim that throws is
2531
- * dropped back to the default for that hook and reported to `onError`, because
2532
- * a replaceable renderer is otherwise a way for one host to ship a broken pad to
2533
- * every game it runs at once.
2534
- *
2535
- * **The SDK declares the version and the shim adapts.** This file is frozen
2536
- * inside bundles that shipped months before any given shim was written, so the
2537
- * dangerous direction is a new shim meeting an old SDK — and it is safe by
2538
- * construction if the shim is the side doing the adapting. A shim reads
2539
- * `request.contract` and answers with what that version can carry.
2174
+ * What one player holds: a pad, one part of the keyboard, or the on-screen
2175
+ * controls.
2540
2176
  *
2541
2177
  * @group Input
2542
2178
  */
2543
- type InputShim = {
2544
- /** The contract version this shim was written against. `1` today. */
2545
- v: 1;
2546
- /**
2547
- * Whether to draw controls for this layout at all.
2548
- *
2549
- * The host's half of the answer; the game's half is having asked. Returning
2550
- * nothing leaves the decision alone, which is what a shim that only wanted to
2551
- * restyle should do.
2552
- */
2553
- decide?(request: {
2554
- layout: string;
2555
- frame: Size;
2556
- /** What the host has taken along each edge — see `ControlModel.insets`. */
2557
- insets: Insets;
2558
- contract: number;
2559
- }): boolean | undefined;
2560
- /**
2561
- * How far a stick must move before it counts, 0 to 1.
2562
- *
2563
- * The host's rather than the game's, because it is an accessibility setting
2564
- * before it is a taste: a player whose hands shake needs a wider dead region
2565
- * in every game they open, not in the ones that happened to think of it.
2566
- */
2567
- deadzone?: number;
2568
- /** A tick when a control is pressed. Absent where the platform has none. */
2569
- haptic?(kind: 'press' | 'release'): void;
2570
- /** Called with the root once, to set custom properties or add a stylesheet. */
2571
- style?(root: ShadowRoot, request: {
2572
- layout: string;
2573
- frame: Size;
2574
- }): void;
2575
- /**
2576
- * Draw the controls instead of the SDK.
2577
- *
2578
- * Given the root, the resolved model and the one way back. Returns a teardown,
2579
- * which is called before the next render and when the controls are hidden.
2580
- */
2581
- render?(root: ShadowRoot, model: ControlModel, report: ControlReport): (() => void) | void;
2179
+ type Controller =
2180
+ /** One of the keyboard sets the game declared, from 0, with the title it gave it. */
2181
+ {
2182
+ kind: 'keyboard';
2183
+ set: number;
2184
+ title: string;
2185
+ } |
2186
+ /** A pad, by `Gamepad.index`. */
2187
+ {
2188
+ kind: 'gamepad';
2189
+ pad: number;
2190
+ } | {
2191
+ kind: 'touch';
2582
2192
  };
2583
- /**
2584
- * @group Input
2585
- */
2586
- declare function isInputShim(value: unknown): value is InputShim;
2587
- /**
2588
- * The contract version this SDK speaks. Told to a shim so it can answer in kind.
2589
- *
2590
- * `2` added `insets` — to the model a renderer is given, to what `decide` is
2591
- * asked, and as four custom properties the default sheet spends. Purely
2592
- * additive: a `v: 1` shim that reads none of them behaves exactly as it did.
2593
- *
2594
- * `3` added `axis` to a thumbstick in the model. Additive again: a shim that
2595
- * draws the stick moving both ways still gives the game one axis, because the
2596
- * other is dropped from what it reports.
2597
- *
2598
- * `4` added `sticks` to the model: whether the player wants a thumbstick that
2599
- * comes to their thumb. Additive: a shim that reads nothing of it draws the
2600
- * stick where it always did.
2601
- *
2602
- * @group Input
2603
- */
2604
- declare const INPUT_CONTRACT = 4;
2605
2193
  //#endregion
2606
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/bindings.d.ts
2194
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/index.d.ts
2607
2195
  /**
2608
- * The standard mapping's buttons, by index, as the names this SDK uses.
2609
- *
2610
- * `Gamepad.mapping === 'standard'` is what makes these indices mean anything;
2611
- * a pad that reports anything else is read for its axes and ignored for its
2612
- * buttons, because an unmapped index is a number with no meaning attached.
2613
- *
2614
- * **The left cluster is the d-pad and the right is the faces**, which is what
2615
- * makes `pad.south` cover both *A* and *down* with one word. Compass positions
2616
- * rather than letters throughout: A and B are swapped between the two
2617
- * best-selling controllers, so a name built on them is wrong for half the
2618
- * players who read it.
2196
+ * A binding a game can name on its own, for a prompt about a button that is
2197
+ * not one of its actions: `pad.start`, `key.Space`.
2619
2198
  *
2620
2199
  * @group Input
2621
2200
  */
2622
- declare const PAD_BUTTONS: readonly (string | null)[];
2623
- /**
2624
- * The binding a player would actually use, given what they are holding.
2625
- *
2626
- * A game that draws one prompt for an action with four bindings has to be told
2627
- * which one, and the answer is the one their hands are on. Falls back to the
2628
- * first declared binding, which is the game author's own idea of the primary
2629
- * one.
2630
- *
2631
- * @group Input
2632
- * @category Useful
2633
- */
2634
- declare function preferredBinding(bind: readonly string[], scheme: InputScheme): string | undefined;
2635
- /**
2636
- * Whether a binding is one the player could press with what they are using.
2637
- *
2638
- * @group Input
2639
- * @category Useful
2640
- */
2641
- declare function servesScheme(binding: string, scheme: InputScheme): boolean;
2642
- /**
2643
- * What to write on the screen for one binding.
2644
- *
2645
- * **Best effort, and the host is allowed to do better.** Whether the south face
2646
- * button says *A* or *B* depends on whose controller it is, and
2647
- * `Gamepad.id` is free text that varies by browser and by operating system — so
2648
- * a host that genuinely knows, because it is a native app, sends its own label
2649
- * and this is what fills in when nobody did. Getting it slightly wrong is a
2650
- * prompt a player can still follow; refusing to draw one is a game that never
2651
- * says which button to press.
2652
- *
2653
- * @group Input
2654
- * @category Useful
2655
- */
2656
- declare function labelFor(binding: string, scheme: InputScheme): string;
2657
- //#endregion
2658
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/index.d.ts
2659
- /**
2660
- * How far a stick moves before it counts, where no shim has an opinion.
2661
- *
2662
- * @group Input
2663
- * @category Useful
2664
- */
2665
- declare const DEADZONE = 0.15;
2201
+ type ButtonName = `key.${string}` | `pad.${string}` | `mouse.${string}` | `pointer.${string}` | `touch.${string}`;
2666
2202
  /**
2667
2203
  * What presses each action, and what the player is pressing with: the part of
2668
2204
  * input that is state rather than a stream of presses.
@@ -2673,11 +2209,12 @@ type InputState<Action extends string = string> = {
2673
2209
  /**
2674
2210
  * What presses each action, and what to call it in front of a player.
2675
2211
  *
2676
- * `mapping.shoot.label` is the whole reason this is readable: it is *Z* on a
2677
- * keyboard, a glyph on whatever pad the player picked up, and the label the
2678
- * manifest gave the on-screen button on a touch screen — the name written on
2679
- * whatever the player would press. So a game writes one prompt and is right
2680
- * on all of them. An action can be missing, so keep a fallback.
2212
+ * `mapping.shoot.label` names one binding in text: *Z* on a keyboard, a
2213
+ * letter for whatever pad the player picked up, and the label the manifest
2214
+ * gave the on-screen button on a touch screen. A prompt is better drawn with
2215
+ * `glyph`, which shows every binding, the same on every controller, and
2216
+ * follows a rebinding without being redrawn. An action can be missing, so
2217
+ * keep a fallback.
2681
2218
  */
2682
2219
  readonly mapping: Readonly<Partial<Record<Action, InputBinding>>>;
2683
2220
  /** What the player is using right now. */
@@ -2697,7 +2234,7 @@ type InputListener<Action extends string = string> = (now: InputState<Action>, p
2697
2234
  * @group Input
2698
2235
  * @category Important
2699
2236
  */
2700
- type Input<Action extends string = string, Layout extends string = string> = InputState<Action> & {
2237
+ type Input<Action extends string = string, Layout extends string = string, PlayerAction extends string = Action> = InputState<Action> & {
2701
2238
  /**
2702
2239
  * Run this whenever an action goes down or comes up.
2703
2240
  *
@@ -2737,6 +2274,33 @@ type Input<Action extends string = string, Layout extends string = string> = Inp
2737
2274
  * The pointer's position needs `poll` set; presses and sticks do not.
2738
2275
  */
2739
2276
  read(): InputSnapshot<Action>;
2277
+ /**
2278
+ * A character that draws what presses an action now, in the font `font`
2279
+ * names: every key or button bound to it, for the device in the player's
2280
+ * hands, as a keycap or a pad button.
2281
+ *
2282
+ * hint.textContent = `Press ${hopple.input.glyph('jump')} to jump`;
2283
+ * ctx.font = `20px 'My Font', sans-serif, ${hopple.input.font}`;
2284
+ *
2285
+ * The character is the same for the whole session; what changes is the font
2286
+ * the SDK draws it with, which follows the player's bindings and their
2287
+ * device. So text in the DOM is right without being touched again, and a
2288
+ * canvas is right the next time it is painted. A binding works too —
2289
+ * `glyph('pad.start')` — for a button that is no action of the game's.
2290
+ *
2291
+ * Only in a font stack that names `font`, and names it last: a box
2292
+ * otherwise. Under `hopple dev` a glyph printed anywhere else is reported to
2293
+ * `onError`.
2294
+ */
2295
+ glyph(name: Action | ButtonName): string;
2296
+ /**
2297
+ * What `glyph` draws, in words: `Z or Space`, `bottom face button`. For an
2298
+ * `aria-label`, and for text drawn with no fonts at all. Empty for an action
2299
+ * with nothing bound for this device.
2300
+ */
2301
+ describe(name: Action | ButtonName): string;
2302
+ /** The font family `glyph` draws in: name it, last, wherever a glyph is printed. */
2303
+ readonly font: string;
2740
2304
  /**
2741
2305
  * Stop reading input and let go of everything held.
2742
2306
  *
@@ -2748,6 +2312,63 @@ type Input<Action extends string = string, Layout extends string = string> = Inp
2748
2312
  suspend(): void;
2749
2313
  resume(): void;
2750
2314
  readonly suspended: boolean;
2315
+ /**
2316
+ * The players on this device, each with their own copy of the actions
2317
+ * `input.player` declares, by `slot`. Empty for a game that declares none;
2318
+ * otherwise the first player is always there.
2319
+ *
2320
+ * Frozen and replaced when somebody joins or leaves. `read()`, `onAction`
2321
+ * and `glyph` on the input itself answer for a player action as if every
2322
+ * player pressed it: what a menu anybody may work reads.
2323
+ */
2324
+ readonly players: readonly Player<PlayerAction>[];
2325
+ /** Run this when a player joins or leaves, claims a controller, or picks up another. */
2326
+ onPlayers(listener: (players: readonly Player<PlayerAction>[]) => void): Unsubscribe;
2327
+ /**
2328
+ * Whether a press of a join action on a controller nobody plays with seats
2329
+ * a new player: set it on the screen where players join, and clear it
2330
+ * before play.
2331
+ *
2332
+ * Setting it gives the first player the controller they last used, so the
2333
+ * others' presses on the rest make them players. The first player's first
2334
+ * press, if they have pressed nothing yet, is theirs.
2335
+ */
2336
+ joining: boolean;
2337
+ /**
2338
+ * The most players this device may have now: what the manifest declares, or
2339
+ * fewer where the match being played has no room for more.
2340
+ */
2341
+ readonly mostPlayers: number;
2342
+ };
2343
+ /**
2344
+ * One player on this device: their own actions, read from the controllers
2345
+ * they hold.
2346
+ *
2347
+ * @group Input
2348
+ * @category Important
2349
+ */
2350
+ type Player<Action extends string = string> = {
2351
+ /** Their place, from 0: the lowest free one when they joined, kept until they leave. */
2352
+ readonly slot: number;
2353
+ /**
2354
+ * What they play with: the controller they claimed, or the one they last
2355
+ * used. `null` once their pad has gone away, until a free one is pressed.
2356
+ */
2357
+ readonly controller: Controller | null;
2358
+ /** What they are using now, for what to draw: never which device to read. */
2359
+ readonly scheme: InputScheme;
2360
+ /** What presses each action for them, on the controllers they hear. */
2361
+ readonly mapping: Readonly<Partial<Record<Action, InputBinding>>>;
2362
+ /** Their actions now, and what happened since their last read: once a frame, as `input.read()`. */
2363
+ read(): InputSnapshot<Action>;
2364
+ onAction(action: Action, listener: ActionListener<Action>): Unsubscribe;
2365
+ /** Their sticks: their pad's, or the on-screen one when they hold the touch screen. */
2366
+ onStick(stick: StickName, listener: StickListener): Unsubscribe;
2367
+ /** A character drawing what presses an action for this player, as `input.glyph` draws it. */
2368
+ glyph(action: Action | ButtonName): string;
2369
+ describe(action: Action | ButtonName): string;
2370
+ /** Take them out: what they held is free again. The only player stays. */
2371
+ leave(): void;
2751
2372
  };
2752
2373
  /**
2753
2374
  * The on-screen controls: shown when the game says so *and* the host says they
@@ -2791,7 +2412,7 @@ type Controls<Layout extends string = string> = {
2791
2412
  readonly available: boolean;
2792
2413
  };
2793
2414
  //#endregion
2794
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/lifecycle.d.ts
2415
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/lifecycle.d.ts
2795
2416
  /** @group Lifecycle */
2796
2417
  type LifecycleListener = (phase: Phase, previous: Phase) => void | Promise<void>;
2797
2418
  /**
@@ -2810,7 +2431,7 @@ type Lifecycle = {
2810
2431
  on(listener: LifecycleListener): Unsubscribe;
2811
2432
  };
2812
2433
  //#endregion
2813
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/loop.d.ts
2434
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/loop.d.ts
2814
2435
  /**
2815
2436
  * The host has let the game run its loop, or stopped it. Return a promise to
2816
2437
  * make the host wait.
@@ -2864,7 +2485,36 @@ type Loop = {
2864
2485
  tick(frame: FrameCallback): Unsubscribe;
2865
2486
  };
2866
2487
  //#endregion
2867
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/panel.d.ts
2488
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/panel.d.ts
2489
+ /**
2490
+ * The pages of the host's panel a game has a name for, as the keys of this
2491
+ * interface.
2492
+ *
2493
+ * An interface rather than a union so that a preview can add its own pages to
2494
+ * it, as it adds its own namespace to `GameSdk`: a page only a preview's games
2495
+ * are offered is named only where that preview is installed.
2496
+ *
2497
+ * @group Panels
2498
+ */
2499
+ interface PanelPages {
2500
+ /** What this game is: its title, its author, its description. */
2501
+ about: true;
2502
+ /** Who has scored what, on this game's boards. */
2503
+ scores: true;
2504
+ /** What has been unlocked, and how far along the rest is. */
2505
+ achievements: true;
2506
+ /** What this game is played with, and where a player rebinds it. */
2507
+ input: true;
2508
+ }
2509
+ /**
2510
+ * A page of the host's panel: one of `PanelPages`.
2511
+ *
2512
+ * It grows, and it only ever grows: a name means the same thing for ever,
2513
+ * because a game ships and is never updated.
2514
+ *
2515
+ * @group Panels
2516
+ */
2517
+ type PageId = keyof PanelPages;
2868
2518
  /**
2869
2519
  * Where the host's panel stands over the game.
2870
2520
  *
@@ -2973,7 +2623,7 @@ type Panel = {
2973
2623
  open(page: PageId): void;
2974
2624
  };
2975
2625
  //#endregion
2976
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/pause.d.ts
2626
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/pause.d.ts
2977
2627
  /**
2978
2628
  * The host has asked for a pause, or for the game to carry on.
2979
2629
  *
@@ -3034,7 +2684,7 @@ type Pause = {
3034
2684
  on(listener: PauseListener): Unsubscribe;
3035
2685
  };
3036
2686
  //#endregion
3037
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/resources.d.ts
2687
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/resources.d.ts
3038
2688
  /**
3039
2689
  * The host has let the game hold its resources again, or asked for them back.
3040
2690
  * Return a promise to make the host wait until they have gone, or come back.
@@ -3064,7 +2714,7 @@ type Resources = {
3064
2714
  on(listener: ResourcesListener): Unsubscribe;
3065
2715
  };
3066
2716
  //#endregion
3067
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/save.d.ts
2717
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/save.d.ts
3068
2718
  /**
3069
2719
  * What a game gets back from a read: its own schema number, and its state.
3070
2720
  *
@@ -3103,6 +2753,45 @@ type SaveStatus = {
3103
2753
  * @category Useful
3104
2754
  */
3105
2755
  type SaveListener = (now: SaveStatus, previous: SaveStatus) => void;
2756
+ /**
2757
+ * Make one save of two that went their own ways.
2758
+ *
2759
+ * `mine` is this device's, `theirs` one another device made meanwhile, and
2760
+ * neither was built on the other. Answer with the save to keep in place of
2761
+ * both, on whatever schema `version` it is written in. Throw where the two
2762
+ * cannot be merged — a schema this build does not know — and the host asks the
2763
+ * player which to keep.
2764
+ *
2765
+ * Pure: it is called with two states and must not touch the game's own. Run
2766
+ * while the game loads, its answer is what `read()` resolves with; run while
2767
+ * the game plays, its answer goes to `onMerge` as well.
2768
+ *
2769
+ * @group Save
2770
+ * @category Useful
2771
+ */
2772
+ type SaveMerger = (mine: SaveState, theirs: SaveState) => SaveState;
2773
+ /**
2774
+ * Told the merged save, once a merge has run while the game was playing on
2775
+ * its own: play on from this, which is both devices' progress.
2776
+ *
2777
+ * @group Save
2778
+ * @category Useful
2779
+ */
2780
+ type SaveMergeListener = (merged: SaveState) => void;
2781
+ /**
2782
+ * What a game says about its save at `init`, before the host has asked
2783
+ * anything.
2784
+ *
2785
+ * @group Save
2786
+ * @category Useful
2787
+ */
2788
+ type SaveSetup = {
2789
+ /**
2790
+ * How to make one save of two. Left out, the host asks the player which of
2791
+ * the two to keep. See `SaveMerger`.
2792
+ */
2793
+ merge?: SaveMerger | undefined;
2794
+ };
3106
2795
  /**
3107
2796
  * @group Save
3108
2797
  * @category Important
@@ -3149,11 +2838,20 @@ type Save = {
3149
2838
  }): Promise<void>;
3150
2839
  /** Throw the save away — a game resetting its own progress, at a player's asking. */
3151
2840
  clear(): Promise<void>;
2841
+ /**
2842
+ * Told when the game's `merge` ran while the game was playing: what the
2843
+ * host now keeps is the merged save, and the game should play on from it.
2844
+ *
2845
+ * A merge needed while the game plays waits for its next load unless
2846
+ * something is listening here, because a game that went on playing from its
2847
+ * own state would save over the merge with its next write.
2848
+ */
2849
+ onMerge(listener: SaveMergeListener): Unsubscribe;
3152
2850
  /** The largest save this host will take, in bytes. `SAVE_BYTES` where it said nothing. */
3153
2851
  readonly maxBytes: number;
3154
2852
  };
3155
2853
  //#endregion
3156
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/score.d.ts
2854
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/score.d.ts
3157
2855
  /**
3158
2856
  * Where this player stands on every board, as far as anybody has said.
3159
2857
  *
@@ -3230,7 +2928,7 @@ type Score<Board extends string = string> = {
3230
2928
  }): Promise<ScoreResult>;
3231
2929
  };
3232
2930
  //#endregion
3233
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/share.d.ts
2931
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/share.d.ts
3234
2932
  /**
3235
2933
  * @group Share
3236
2934
  * @category Important
@@ -3302,7 +3000,7 @@ type Share = {
3302
3000
  }): void;
3303
3001
  };
3304
3002
  //#endregion
3305
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/init.d.ts
3003
+ //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/init.d.ts
3306
3004
  /**
3307
3005
  * What `init` hands back: every namespace a game has, on one object.
3308
3006
  *
@@ -3317,7 +3015,7 @@ type Share = {
3317
3015
  * @group Bridge
3318
3016
  * @category Useful
3319
3017
  */
3320
- type GameSdk<M extends object = GameManifest> = {
3018
+ interface GameSdk<M extends object = GameManifest> {
3321
3019
  /**
3322
3020
  * How far the game has got: `loading`, `ready`, `disposed`. The one thing
3323
3021
  * here a game must actually call is `lifecycle.ready()`.
@@ -3433,7 +3131,7 @@ type GameSdk<M extends object = GameManifest> = {
3433
3131
  *
3434
3132
  * @category Useful
3435
3133
  */
3436
- input: Input<ActionName<M>, LayoutName<M>>;
3134
+ input: Input<ActionName<M> | PlayerActionName<M>, LayoutName<M>, PlayerActionName<M>>;
3437
3135
  /**
3438
3136
  * How loud this game may be, which the host decides and the game obeys.
3439
3137
  *
@@ -3526,19 +3224,17 @@ type GameSdk<M extends object = GameManifest> = {
3526
3224
  *
3527
3225
  * Resolves with the round trip in milliseconds, rejecting with a `BridgeError`
3528
3226
  * whose code is `timeout` if the host never answers. This is the liveness
3529
- * check the demo is built on, and it stays useful afterwards.
3227
+ * check, and what a game can measure the bridge with.
3530
3228
  */
3531
3229
  ping(): Promise<number>;
3532
3230
  /** Tear the bridge down. Calls in flight reject with `disposed`. */
3533
3231
  dispose(): void;
3534
- };
3232
+ }
3535
3233
  /**
3536
3234
  * @group Bridge
3537
3235
  * @category Useful
3538
3236
  */
3539
3237
  type InitOptions = {
3540
- /** The window the game is running in. Defaults to the global one. */
3541
- scope?: Window | undefined;
3542
3238
  /**
3543
3239
  * How long a call waits before rejecting with `timeout`.
3544
3240
  *
@@ -3548,22 +3244,6 @@ type InitOptions = {
3548
3244
  * in it.
3549
3245
  */
3550
3246
  timeoutMs?: number | undefined;
3551
- /**
3552
- * The channel to talk over. Defaults to whatever the game turns out to be
3553
- * running in — a shim a native shell left, or the frame that embedded us —
3554
- * which is what every real game wants; a test, or a host that embeds a game
3555
- * some other way, can hand over its own.
3556
- */
3557
- transport?: Transport | undefined;
3558
- /**
3559
- * How long to wait for a host to turn up when there is neither a shim nor a
3560
- * parent frame, before rejecting. Defaults to `SHIM_WAIT_MS`.
3561
- *
3562
- * Here from day one rather than when a platform first needs it: this is
3563
- * game-side code, bundled into a game at build time, so it can never be added
3564
- * later for games already published.
3565
- */
3566
- shimWaitMs?: number | undefined;
3567
3247
  /**
3568
3248
  * Where a failure that belongs to nobody's promise goes — a lifecycle
3569
3249
  * listener that threw, an event the SDK refused. Defaults to `console.error`.
@@ -3572,6 +3252,15 @@ type InitOptions = {
3572
3252
  * them, as it is about any error the game throws and nothing catches.
3573
3253
  */
3574
3254
  onError?: ((error: unknown) => void) | undefined;
3255
+ /**
3256
+ * What the game says about its save before the host asks anything: how to
3257
+ * merge two saves that went their own ways, on two devices. See `SaveSetup`.
3258
+ *
3259
+ * Here rather than on `sdk.save`, because the host may ask while the game is
3260
+ * still reading its save — the merge is what that read resolves with — and a
3261
+ * merge set after the read would be one the host never heard of.
3262
+ */
3263
+ save?: SaveSetup | undefined;
3575
3264
  };
3576
3265
  /**
3577
3266
  * Introduce the game to its host, and hand back the SDK once it has answered.
@@ -3607,62 +3296,4 @@ type InitOptions = {
3607
3296
  */
3608
3297
  declare function init<M extends object = GameManifest>(options?: InitOptions): Promise<GameSdk<M>>;
3609
3298
  //#endregion
3610
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/transport.d.ts
3611
- /**
3612
- * How long a game with no shim and no parent waits for one to turn up.
3613
- *
3614
- * Generous on purpose: a shim two seconds late is not coming, and the only game
3615
- * made to wait is one that was never going to run.
3616
- *
3617
- * @group Bridge
3618
- */
3619
- declare const SHIM_WAIT_MS = 2000;
3620
- /** @group Bridge */
3621
- type HostTransportOptions = {
3622
- /** The window the game is running in. Defaults to the global one. */
3623
- scope?: Window | undefined;
3624
- /** How long to wait for a late shim before giving up. */
3625
- shimWaitMs?: number | undefined;
3626
- /** Where a shim that arrived wrongly, or twice, is reported. */
3627
- onError?: ((error: unknown) => void) | undefined;
3628
- };
3629
- /**
3630
- * Find something to talk over, waiting briefly if there is nothing yet.
3631
- *
3632
- * Asynchronous because of that last case alone. The two ordinary ones — a shim
3633
- * already installed, a parent frame — resolve without yielding a turn.
3634
- *
3635
- * @group Bridge
3636
- * @category Useful
3637
- */
3638
- declare function hostTransport(options?: HostTransportOptions): Promise<Transport>;
3639
- //#endregion
3640
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/shim.d.ts
3641
- /**
3642
- * Whether an arbitrary value is a shim this SDK knows how to speak to.
3643
- *
3644
- * A version we do not know is a shim we must not guess at — a `v: 2` that moved
3645
- * `send` to a different signature would be a channel we corrupt rather than one
3646
- * we fail on. Refusing it falls through to the frame branch, and then to the
3647
- * wait, which either connects or says clearly that nothing did.
3648
- *
3649
- * @group Bridge
3650
- */
3651
- declare function isShim(value: unknown): value is HoppleShim;
3652
- /**
3653
- * What a platform left for us, or nothing — in which case we are in a frame.
3654
- *
3655
- * @group Bridge
3656
- */
3657
- declare function findShim(scope: Window): HoppleShim | null;
3658
- /**
3659
- * A shim, seen by the peer as an ordinary channel.
3660
- *
3661
- * The trust boundary is in here, in SDK code, and never in the platform's: a
3662
- * shim that cannot validate is a shim that cannot validate wrongly.
3663
- *
3664
- * @group Bridge
3665
- */
3666
- declare function shimTransport(shim: HoppleShim): Transport;
3667
- //#endregion
3668
- export { ACHIEVEMENT_VALUE_MAX, AUDIO_FULL, AUDIO_MASTER, AUDIO_VOLUME_MAX, type Achievement, type AchievementDeclaration, type AchievementListener, type AchievementName, type AchievementProgress, type AchievementResult, type AchievementStanding, type AchievementStatus, type AchievementTier, type Achievements, type ActionListener, type ActionName, type Analytics, type AnalyticsDeclaration, type AnalyticsProps, type AnalyticsShape, type AnalyticsType, type AnalyticsValue, type AppInfo, type AppSurface, type Audio, type AudioChannel, type AudioDeclaration, type AudioListener, type AudioState, type BoardName, BridgeError, type ChannelName, type ControlLayout, type ControlModel, type ControlModelSlot, type ControlModelZone, type ControlReport, type ControlShape, type ControlSlot, type ControlZone, type Controls, DEADZONE, type DeclaredNames, type DeclaredValue, type DeviceInfo, type DeviceTier, type ErrorCode, type EventName, type EventProps, type FrameCallback, type GameInfo, type GameManifest, type GameSdk, type HoppleConfig, type HoppleShim, type HostTransportOptions, INPUT_CONTRACT, INPUT_SCHEMES, INPUT_SHIM, type Info, type InitOptions, type Input, type InputAction, type InputBinding, type InputChangeCause, type InputDeclaration, type InputDevice, type InputListener, type InputScheme, type InputShim, type InputSnapshot, type InputState, type InsetMode, type InsetPolicy, type Insets, type Layout, type LayoutDeclaration, type LayoutListener, type LayoutName, type LayoutState, type Lifecycle, type LifecycleListener, type Loop, type LoopListener, MANIFEST_FILE, MANIFEST_SCHEMA, MAX_STEP, type MeasuredName, type Obstruction, type ObstructionKind, type Orientation, PAD_BUTTONS, PAGES, PAUSE_NONE, PAUSE_STATES, type PageId, type Panel, type PanelListener, type PanelState, type Pause, type PauseListener, type PauseState, type Phase, type PlayerInfo, type PointerListener, type PointerState, type Rect, type Resources, type ResourcesListener, SAVE_BYTES, SCORE_VALUE_MAX, SET_SHIM, SHARE_BYTES, SHARE_META_BYTES, SHIM, SHIM_WAIT_MS, type Save, type SaveDeclaration, type SaveListener, type SaveState, type SaveStatus, type SaveStored, type Score, type ScoreBoard, type ScoreDeclaration, type ScoreDirection, type ScoreFormat, type ScoreListener, type ScoreResult, type ScoreStanding, type ScoreStandings, type Share, type ShareMeta, type ShareMetaValue, type Size, type StickListener, type StickName, type StickState, type Unsubscribe, audioVolumeOf, findShim, hostTransport, init, insetsFrom, isInputShim, isShim, labelFor, preferredBinding, servesScheme, shimTransport };
3299
+ export { type Achievement, type AchievementDeclaration, type AchievementListener, type AchievementName, type AchievementProgress, type AchievementStanding, type AchievementStatus, type AchievementTier, type Achievements, type ActionListener, type ActionName, type Analytics, type AnalyticsDeclaration, type AnalyticsProps, type AnalyticsShape, type AnalyticsType, type AnalyticsValue, type ApiVersion, type AppInfo, type AppSurface, type Audio, type AudioChannel, type AudioDeclaration, type AudioListener, type BoardName, BridgeError, type ButtonName, type ChannelName, type ControlLayout, type ControlShape, type ControlSlot, type ControlZone, type Controller, type Controls, type DeclaredNames, type DeclaredValue, type DeviceInfo, type DeviceTier, type ErrorCode, type ErrorCodes, type EventName, type EventProps, type FrameCallback, type GameInfo, type GameManifest, type GameSdk, type HoppleConfig, type Info, type InitOptions, type Input, type InputAction, type InputBinding, type InputDeclaration, type InputDevice, type InputListener, type InputScheme, type InputSnapshot, type InputState, type InsetMode, type InsetPolicy, type Insets, type Layout, type LayoutDeclaration, type LayoutListener, type LayoutName, type LayoutState, type Lifecycle, type LifecycleListener, type Loop, type LoopListener, MAX_STEP, type MeasuredName, type Obstruction, type ObstructionKind, type Orientation, type PageId, type Panel, type PanelListener, type PanelPages, type PanelState, type Pause, type PauseListener, type PauseState, type Phase, type Player, type PlayerActionName, type PlayerInfo, type PointerListener, type PointerState, type ProjectManifest, type Rect, type Resources, type ResourcesListener, SAVE_BYTES, SHARE_BYTES, SHARE_META_BYTES, type Save, type SaveDeclaration, type SaveListener, type SaveMergeListener, type SaveMerger, type SaveSetup, type SaveState, type SaveStatus, type SaveStored, type Score, type ScoreBoard, type ScoreDeclaration, type ScoreDirection, type ScoreFormat, type ScoreListener, type ScoreResult, type ScoreStanding, type ScoreStandings, type Share, type ShareMeta, type ShareMetaValue, type Size, type StickListener, type StickName, type StickState, type Unsubscribe, init, insetsFrom };