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

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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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-A6KFyv/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
633
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/manifest/src/manifest.d.ts
1026
634
  /**
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";
1062
- /**
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,20 @@ 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;
1312
+ /**
1313
+ * How big this control is next to the layout's others: `1.25` for a jump
1314
+ * button a thumb lands on without looking, `0.8` for one pressed now and then.
1315
+ * Absent, 1.
1316
+ *
1317
+ * A size in the layout rather than in the game's stylesheet, because whether
1318
+ * the layout fits on a small screen is worked out from it before the
1319
+ * controls are drawn. A game's stylesheet changes how they look, never how
1320
+ * much of the screen they answer in.
1321
+ *
1322
+ * @minimum 0.75
1323
+ * @maximum 1.5
1324
+ */
1325
+ scale?: number;
1645
1326
  };
1646
1327
  /**
1647
1328
  * The one direction an on-screen thumbstick moves in: `x` across, `y` up and
@@ -1674,8 +1355,8 @@ type ControlZoneSlots = Record<string, ControlSlot>;
1674
1355
  * A game may declare several and switch between them — a menu wants different
1675
1356
  * controls from play, and a driving section different ones again — which is why
1676
1357
  * 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.
1358
+ * from a newer minor, which a host reads leniently, so whoever draws it simply
1359
+ * has nowhere to put it.
1679
1360
  *
1680
1361
  * @group Input
1681
1362
  */
@@ -1683,7 +1364,6 @@ type ControlLayout = {
1683
1364
  left?: ControlZoneSlots;
1684
1365
  right?: ControlZoneSlots;
1685
1366
  full?: ControlZoneSlots;
1686
- [key: string]: unknown;
1687
1367
  };
1688
1368
  /**
1689
1369
  * What a game says about how it is played.
@@ -1733,20 +1413,99 @@ type InputDeclaration = {
1733
1413
  * @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
1414
  */
1735
1415
  controls?: Record<string, ControlLayout>;
1736
- [key: string]: unknown;
1416
+ /**
1417
+ * What each local player does, beside `actions`, which are everybody's. See
1418
+ * `PlayerInputDeclaration`.
1419
+ */
1420
+ player?: PlayerInputDeclaration;
1421
+ };
1422
+ /**
1423
+ * How many people play a game on one device.
1424
+ *
1425
+ * Read by hosts before the game runs: a listing says *1–4 on one device*, and
1426
+ * the input panel offers a row for each player.
1427
+ *
1428
+ * @group Input
1429
+ * @category Useful
1430
+ */
1431
+ type PlayersDeclaration = {
1432
+ /**
1433
+ * The most players on one device, each with their own copy of
1434
+ * `input.player`'s actions. 1 when left out.
1435
+ *
1436
+ * @minimum 1
1437
+ * @maximum 8
1438
+ * @asType integer
1439
+ */
1440
+ local?: number;
1737
1441
  };
1738
1442
  /**
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.
1443
+ * A preview's revision: a whole number from 1, the `R` of its package's
1444
+ * `0.R.p`. A breaking change to the preview is a new revision.
1445
+ *
1446
+ * @group Manifest
1447
+ * @category Useful
1448
+ * @minimum 1
1449
+ * @asType integer
1450
+ */
1451
+ type PreviewRevision = number;
1452
+ /**
1453
+ * The previews a game is written against, each at the revision it needs.
1454
+ *
1455
+ * A preview is a part of the SDK that is not stable yet: its own `0.x`
1456
+ * package, versioned apart from `api`, and served only to the games that
1457
+ * declare it, at the revision they declare. A change to a preview reaches the
1458
+ * games written against it and nobody else.
1459
+ *
1460
+ * @group Manifest
1461
+ * @category Useful
1462
+ */
1463
+ type PreviewsDeclaration = {
1464
+ /**
1465
+ * That this game plays over the network, in matches, at this revision of
1466
+ * `@hopple/sdk-preview-network`: `sdk.match`.
1467
+ *
1468
+ * The permission a reviewer grants, and what lets a host offer *Play with
1469
+ * friends* before the game has loaded. Nothing else about networking is
1470
+ * declared: the way a match is played, and its size, are chosen by the game
1471
+ * when it makes one. Absent, a game is never told about a match, and never
1472
+ * reaches one.
1473
+ */
1474
+ network?: PreviewRevision;
1475
+ };
1476
+ /**
1477
+ * What kind of game this is, as one word from a list every game shares.
1478
+ *
1479
+ * A closed list, because a client files games under these and has to name each
1480
+ * one in front of a player: left to free text, one kind of game becomes three
1481
+ * spellings of it with a game each. The list only grows, since a published
1482
+ * manifest has to stay valid. Whoever reads one may be older than the game, and
1483
+ * ignores a word it does not know.
1484
+ *
1485
+ * `local-multiplayer` and `online-multiplayer` say how a game is played rather
1486
+ * than what it is, and only count where the manifest bears them out: see
1487
+ * `declaredGenres`.
1488
+ *
1489
+ * @group Manifest
1490
+ * @category Useful
1491
+ */
1492
+ type GameGenre = 'racing' | 'shooter' | 'platformer' | 'puzzle' | 'arcade' | 'physics' | 'roguelike' | 'strategy' | 'building' | 'idle' | 'sports' | 'horror' | 'stealth' | 'fighting' | 'casual' | 'simulation' | 'adventure' | 'rhythm' | 'rpg' | 'local-multiplayer' | 'online-multiplayer';
1493
+ /**
1494
+ * Publishing refuses a key the manifest's API does not know; a host reading a
1495
+ * published one ignores it (see `validateManifest`).
1743
1496
  *
1744
1497
  * @group Manifest
1745
1498
  * @category Important
1746
1499
  */
1747
1500
  type GameManifest = {
1748
- /** @category Important */
1749
- schema: typeof MANIFEST_SCHEMA;
1501
+ /**
1502
+ * The API the game is written against. The major picks what the game is
1503
+ * served; the minor is the least it needs. The CLI fills it from the
1504
+ * installed `@hopple/sdk` when the config leaves it out.
1505
+ *
1506
+ * @category Important
1507
+ */
1508
+ api: ApiVersion;
1750
1509
  /**
1751
1510
  * Stable across versions, and unique among its publisher's games: it is the
1752
1511
  * second half of the address the game is published at, `owner/id`, and the
@@ -1770,6 +1529,15 @@ type GameManifest = {
1770
1529
  * @errorMessage must be a handle: lowercase letters, digits and hyphens
1771
1530
  */
1772
1531
  owner?: string;
1532
+ /**
1533
+ * `false` for a game that is never to be uploaded: an example, a test, a
1534
+ * game kept to one machine. `hopple dev` and `hopple check` still run it;
1535
+ * `hopple upload` and `hopple publish` refuse it, and so does a server it
1536
+ * is sent to another way. Absent, it may be published.
1537
+ *
1538
+ * @category Useful
1539
+ */
1540
+ publishable?: boolean;
1773
1541
  /**
1774
1542
  * The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
1775
1543
  *
@@ -1794,6 +1562,18 @@ type GameManifest = {
1794
1562
  * @errorMessage must not be blank
1795
1563
  */
1796
1564
  description?: string;
1565
+ /**
1566
+ * What kind of game it is, the main one first. Read by whoever files it in a
1567
+ * catalogue, which never runs it. At most `GENRE_MAX`, not counting
1568
+ * `local-multiplayer` and `online-multiplayer`: a game that is every kind
1569
+ * tells a player nothing. The publisher's claim, taken at its word, except
1570
+ * for those two. See `GameGenre`.
1571
+ *
1572
+ * @category Useful
1573
+ * @minItems 1
1574
+ * @maxItems 5
1575
+ */
1576
+ genres?: GameGenre[];
1797
1577
  /**
1798
1578
  * A picture of the game, shown wherever a link to it is pasted — a chat, a
1799
1579
  * post, a message.
@@ -1811,22 +1591,6 @@ type GameManifest = {
1811
1591
  cover?: string;
1812
1592
  /** @category Important */
1813
1593
  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
1594
  /**
1831
1595
  * How the game wants the host's furniture handled. See `InsetMode`.
1832
1596
  *
@@ -1863,6 +1627,12 @@ type GameManifest = {
1863
1627
  * @category Useful
1864
1628
  */
1865
1629
  input?: InputDeclaration;
1630
+ /**
1631
+ * How many play on one device. See `PlayersDeclaration`.
1632
+ *
1633
+ * @category Useful
1634
+ */
1635
+ players?: PlayersDeclaration;
1866
1636
  /**
1867
1637
  * What this game makes noise with, one channel at a time, or `null` for a
1868
1638
  * game that makes none. See `AudioDeclaration`.
@@ -1870,7 +1640,13 @@ type GameManifest = {
1870
1640
  * @category Important
1871
1641
  */
1872
1642
  audio: AudioDeclaration | null;
1873
- [key: string]: unknown;
1643
+ /**
1644
+ * The previews this game is written against, each at its revision. See
1645
+ * `PreviewsDeclaration`.
1646
+ *
1647
+ * @category Useful
1648
+ */
1649
+ previews?: PreviewsDeclaration;
1874
1650
  };
1875
1651
  /**
1876
1652
  * A game project's hopple config: `hopple.config.ts`, `hopple.config.js` or
@@ -1888,7 +1664,7 @@ type GameManifest = {
1888
1664
  * export default {
1889
1665
  * build: 'npm run build',
1890
1666
  * 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 },
1667
+ * manifest: { id: 'my-game', version: '0.1.0', title: 'My Game', orientation: 'portrait', audio: null },
1892
1668
  * } satisfies HoppleConfig;
1893
1669
  * ```
1894
1670
  *
@@ -1908,18 +1684,29 @@ type HoppleConfig = {
1908
1684
  */
1909
1685
  build?: string;
1910
1686
  /**
1911
- * The built page, relative to the config: the one file that is published,
1912
- * so it has to hold the whole game.
1687
+ * The built page, relative to the config. The directory it is in is
1688
+ * published, with the page as its `index.html`; a page beside its config is
1689
+ * published alone, so it has to hold the whole game.
1913
1690
  *
1914
1691
  * @pattern \.[Hh][Tt][Mm][Ll]?$
1915
1692
  * @errorMessage must be the built .html file, like "dist/index.html"
1916
1693
  */
1917
1694
  page: string;
1918
1695
  /** What the game is. Uploaded beside the page, and read by every host before the game runs. */
1919
- manifest: GameManifest;
1696
+ manifest: ProjectManifest;
1697
+ };
1698
+ /**
1699
+ * The manifest as a project's config holds it: `api` may be left out, for the
1700
+ * CLI to fill from the installed `@hopple/sdk`.
1701
+ *
1702
+ * @group Manifest
1703
+ * @category Useful
1704
+ */
1705
+ type ProjectManifest = Omit<GameManifest, 'api'> & {
1706
+ api?: ApiVersion;
1920
1707
  };
1921
1708
  //#endregion
1922
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/achievements.d.ts
1709
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/achievements.d.ts
1923
1710
  /**
1924
1711
  * Where this player stands on one achievement.
1925
1712
  *
@@ -2017,7 +1804,7 @@ type Achievements<Id extends string = string, Measured extends string = Id> = {
2017
1804
  progress(id: Measured, value: number): void;
2018
1805
  };
2019
1806
  //#endregion
2020
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/analytics.d.ts
1807
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/analytics.d.ts
2021
1808
  /**
2022
1809
  * @group Analytics
2023
1810
  * @category Important
@@ -2034,7 +1821,7 @@ type Analytics<Events extends object = Record<string, AnalyticsProps>> = {
2034
1821
  track<Event extends keyof Events & string>(name: Event, props?: Events[Event]): void;
2035
1822
  };
2036
1823
  //#endregion
2037
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/audio.d.ts
1824
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/audio.d.ts
2038
1825
  /**
2039
1826
  * Something about the game's sound changed: the host's order, the player's
2040
1827
  * mute, a fader. Read what you need — `volume(channel)`, `allowed` — and apply
@@ -2066,8 +1853,6 @@ type Audio<Channel extends string = string> = {
2066
1853
  readonly muted: boolean;
2067
1854
  /** The fader over the whole game, `0`–`1`, ignoring mute. */
2068
1855
  readonly master: number;
2069
- /** The whole of what the host last said. Frozen; a new object on every change. */
2070
- readonly state: AudioState;
2071
1856
  /**
2072
1857
  * How loud one of this game's channels should actually come out, `0`–`1`.
2073
1858
  *
@@ -2127,7 +1912,7 @@ type Audio<Channel extends string = string> = {
2127
1912
  media(element: HTMLMediaElement, channel?: Channel): Unsubscribe;
2128
1913
  };
2129
1914
  //#endregion
2130
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/info.d.ts
1915
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/info.d.ts
2131
1916
  /**
2132
1917
  * What the host said, frozen.
2133
1918
  *
@@ -2145,7 +1930,7 @@ type Info = Readonly<{
2145
1930
  device: Readonly<DeviceInfo>;
2146
1931
  }>;
2147
1932
  //#endregion
2148
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/layout.d.ts
1933
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/layout.d.ts
2149
1934
  /**
2150
1935
  * How much of each edge belongs to the host. CSS pixels.
2151
1936
  *
@@ -2255,7 +2040,7 @@ type InsetPolicy = {
2255
2040
  */
2256
2041
  declare function insetsFrom(obstructions: readonly Obstruction[], frame: Size, policy?: InsetPolicy): Insets;
2257
2042
  //#endregion
2258
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/names.d.ts
2043
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/names.d.ts
2259
2044
  /**
2260
2045
  * The keys of a declared map, or `string` where the map is not a literal one —
2261
2046
  * which is what a manifest typed as the wide `GameManifest` gives — and
@@ -2274,6 +2059,19 @@ type ActionName<M> = M extends {
2274
2059
  actions?: infer A;
2275
2060
  };
2276
2061
  } ? DeclaredNames<NonNullable<A>> : never;
2062
+ /**
2063
+ * An action each local player has, as the manifest declares it under
2064
+ * `input.player.actions`.
2065
+ *
2066
+ * @group Names
2067
+ */
2068
+ type PlayerActionName<M> = M extends {
2069
+ input?: {
2070
+ player?: {
2071
+ actions?: infer A;
2072
+ };
2073
+ };
2074
+ } ? DeclaredNames<NonNullable<A>> : never;
2277
2075
  /**
2278
2076
  * An on-screen control layout the manifest declares under `input.controls`.
2279
2077
  *
@@ -2362,7 +2160,7 @@ type EventProps<M> = M extends {
2362
2160
  */
2363
2161
  type StickName = 'pad.left.stick' | 'pad.right.stick' | 'pad.stick' | `controls.${string}`;
2364
2162
  //#endregion
2365
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/state.d.ts
2163
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/state.d.ts
2366
2164
  /**
2367
2165
  * Where the pointer is, in the frame's own coordinates.
2368
2166
  *
@@ -2423,246 +2221,36 @@ type InputSnapshot<Action extends string = string> = {
2423
2221
  pointer: PointerState | null;
2424
2222
  };
2425
2223
  //#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
- };
2224
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/players.d.ts
2467
2225
  /**
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.
2226
+ * What one player holds: a pad, one part of the keyboard, or the on-screen
2227
+ * controls.
2476
2228
  *
2477
2229
  * @group Input
2478
2230
  */
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
- };
2527
- /**
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.
2540
- *
2541
- * @group Input
2542
- */
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;
2231
+ type Controller =
2232
+ /** One of the keyboard sets the game declared, from 0, with the title it gave it. */
2233
+ {
2234
+ kind: 'keyboard';
2235
+ set: number;
2236
+ title: string;
2237
+ } |
2238
+ /** A pad, by `Gamepad.index`. */
2239
+ {
2240
+ kind: 'gamepad';
2241
+ pad: number;
2242
+ } | {
2243
+ kind: 'touch';
2582
2244
  };
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
- //#endregion
2606
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/bindings.d.ts
2607
- /**
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.
2619
- *
2620
- * @group Input
2621
- */
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
2245
  //#endregion
2658
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/input/index.d.ts
2246
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/index.d.ts
2659
2247
  /**
2660
- * How far a stick moves before it counts, where no shim has an opinion.
2248
+ * A binding a game can name on its own, for a prompt about a button that is
2249
+ * not one of its actions: `pad.start`, `key.Space`.
2661
2250
  *
2662
2251
  * @group Input
2663
- * @category Useful
2664
2252
  */
2665
- declare const DEADZONE = 0.15;
2253
+ type ButtonName = `key.${string}` | `pad.${string}` | `mouse.${string}` | `pointer.${string}` | `touch.${string}`;
2666
2254
  /**
2667
2255
  * What presses each action, and what the player is pressing with: the part of
2668
2256
  * input that is state rather than a stream of presses.
@@ -2673,11 +2261,12 @@ type InputState<Action extends string = string> = {
2673
2261
  /**
2674
2262
  * What presses each action, and what to call it in front of a player.
2675
2263
  *
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.
2264
+ * `mapping.shoot.label` names one binding in text: *Z* on a keyboard, a
2265
+ * letter for whatever pad the player picked up, and the label the manifest
2266
+ * gave the on-screen button on a touch screen. A prompt is better drawn with
2267
+ * `glyph`, which shows every binding, the same on every controller, and
2268
+ * follows a rebinding without being redrawn. An action can be missing, so
2269
+ * keep a fallback.
2681
2270
  */
2682
2271
  readonly mapping: Readonly<Partial<Record<Action, InputBinding>>>;
2683
2272
  /** What the player is using right now. */
@@ -2697,7 +2286,7 @@ type InputListener<Action extends string = string> = (now: InputState<Action>, p
2697
2286
  * @group Input
2698
2287
  * @category Important
2699
2288
  */
2700
- type Input<Action extends string = string, Layout extends string = string> = InputState<Action> & {
2289
+ type Input<Action extends string = string, Layout extends string = string, PlayerAction extends string = Action> = InputState<Action> & {
2701
2290
  /**
2702
2291
  * Run this whenever an action goes down or comes up.
2703
2292
  *
@@ -2737,6 +2326,49 @@ type Input<Action extends string = string, Layout extends string = string> = Inp
2737
2326
  * The pointer's position needs `poll` set; presses and sticks do not.
2738
2327
  */
2739
2328
  read(): InputSnapshot<Action>;
2329
+ /**
2330
+ * A character that draws what presses an action now, in the font `font`
2331
+ * names: every key or button bound to it, for the device in the player's
2332
+ * hands, as a keycap or a pad button.
2333
+ *
2334
+ * hint.textContent = `Press ${hopple.input.glyph('jump')} to jump`;
2335
+ * ctx.font = `20px 'My Font', sans-serif, ${hopple.input.font}`;
2336
+ *
2337
+ * The character is the same for the whole session; what changes is the font
2338
+ * the SDK draws it with, which follows the player's bindings and their
2339
+ * device. So text in the DOM is right without being touched again, and a
2340
+ * canvas is right the next time it is painted. A binding works too —
2341
+ * `glyph('pad.start')` — for a button that is no action of the game's.
2342
+ *
2343
+ * Only in a font stack that names `font`, and names it last: a box
2344
+ * otherwise. Under `hopple dev` a glyph printed anywhere else is reported to
2345
+ * `onError`.
2346
+ */
2347
+ glyph(name: Action | ButtonName): string;
2348
+ /**
2349
+ * What `glyph` draws, in words: `Z or Space`, `bottom face button`. For an
2350
+ * `aria-label`, and for text drawn with no fonts at all. Empty for an action
2351
+ * with nothing bound for this device.
2352
+ */
2353
+ describe(name: Action | ButtonName): string;
2354
+ /** The font family `glyph` draws in: name it, last, wherever a glyph is printed. */
2355
+ readonly font: string;
2356
+ /**
2357
+ * Draw an icon on an action's on-screen button instead of its label, or
2358
+ * `null` to go back to the label.
2359
+ *
2360
+ * hopple.input.icon('jump', 'M12 3 4 13h5v8h6v-8h5z');
2361
+ *
2362
+ * One SVG path, filled in the buttons' ink, in a 24 by 24 box: what icon
2363
+ * sets draw filled icons in. The label stays the button's name for a screen
2364
+ * reader. `glyph(action)` on a touch screen draws the same icon, so a prompt
2365
+ * matches the button. A path that cannot be read, or that leaves the box, is
2366
+ * reported to `onError` and the label is drawn instead.
2367
+ *
2368
+ * How the controls look otherwise is the game's stylesheet's: see the
2369
+ * skill's input guide.
2370
+ */
2371
+ icon(action: Action, path: string | null): void;
2740
2372
  /**
2741
2373
  * Stop reading input and let go of everything held.
2742
2374
  *
@@ -2748,6 +2380,63 @@ type Input<Action extends string = string, Layout extends string = string> = Inp
2748
2380
  suspend(): void;
2749
2381
  resume(): void;
2750
2382
  readonly suspended: boolean;
2383
+ /**
2384
+ * The players on this device, each with their own copy of the actions
2385
+ * `input.player` declares, by `slot`. Empty for a game that declares none;
2386
+ * otherwise the first player is always there.
2387
+ *
2388
+ * Frozen and replaced when somebody joins or leaves. `read()`, `onAction`
2389
+ * and `glyph` on the input itself answer for a player action as if every
2390
+ * player pressed it: what a menu anybody may work reads.
2391
+ */
2392
+ readonly players: readonly Player<PlayerAction>[];
2393
+ /** Run this when a player joins or leaves, claims a controller, or picks up another. */
2394
+ onPlayers(listener: (players: readonly Player<PlayerAction>[]) => void): Unsubscribe;
2395
+ /**
2396
+ * Whether a press of a join action on a controller nobody plays with seats
2397
+ * a new player: set it on the screen where players join, and clear it
2398
+ * before play.
2399
+ *
2400
+ * Setting it gives the first player the controller they last used, so the
2401
+ * others' presses on the rest make them players. The first player's first
2402
+ * press, if they have pressed nothing yet, is theirs.
2403
+ */
2404
+ joining: boolean;
2405
+ /**
2406
+ * The most players this device may have now: what the manifest declares, or
2407
+ * fewer where the match being played has no room for more.
2408
+ */
2409
+ readonly mostPlayers: number;
2410
+ };
2411
+ /**
2412
+ * One player on this device: their own actions, read from the controllers
2413
+ * they hold.
2414
+ *
2415
+ * @group Input
2416
+ * @category Important
2417
+ */
2418
+ type Player<Action extends string = string> = {
2419
+ /** Their place, from 0: the lowest free one when they joined, kept until they leave. */
2420
+ readonly slot: number;
2421
+ /**
2422
+ * What they play with: the controller they claimed, or the one they last
2423
+ * used. `null` once their pad has gone away, until a free one is pressed.
2424
+ */
2425
+ readonly controller: Controller | null;
2426
+ /** What they are using now, for what to draw: never which device to read. */
2427
+ readonly scheme: InputScheme;
2428
+ /** What presses each action for them, on the controllers they hear. */
2429
+ readonly mapping: Readonly<Partial<Record<Action, InputBinding>>>;
2430
+ /** Their actions now, and what happened since their last read: once a frame, as `input.read()`. */
2431
+ read(): InputSnapshot<Action>;
2432
+ onAction(action: Action, listener: ActionListener<Action>): Unsubscribe;
2433
+ /** Their sticks: their pad's, or the on-screen one when they hold the touch screen. */
2434
+ onStick(stick: StickName, listener: StickListener): Unsubscribe;
2435
+ /** A character drawing what presses an action for this player, as `input.glyph` draws it. */
2436
+ glyph(action: Action | ButtonName): string;
2437
+ describe(action: Action | ButtonName): string;
2438
+ /** Take them out: what they held is free again. The only player stays. */
2439
+ leave(): void;
2751
2440
  };
2752
2441
  /**
2753
2442
  * The on-screen controls: shown when the game says so *and* the host says they
@@ -2791,7 +2480,7 @@ type Controls<Layout extends string = string> = {
2791
2480
  readonly available: boolean;
2792
2481
  };
2793
2482
  //#endregion
2794
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/lifecycle.d.ts
2483
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/lifecycle.d.ts
2795
2484
  /** @group Lifecycle */
2796
2485
  type LifecycleListener = (phase: Phase, previous: Phase) => void | Promise<void>;
2797
2486
  /**
@@ -2810,7 +2499,7 @@ type Lifecycle = {
2810
2499
  on(listener: LifecycleListener): Unsubscribe;
2811
2500
  };
2812
2501
  //#endregion
2813
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/loop.d.ts
2502
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/loop.d.ts
2814
2503
  /**
2815
2504
  * The host has let the game run its loop, or stopped it. Return a promise to
2816
2505
  * make the host wait.
@@ -2864,7 +2553,36 @@ type Loop = {
2864
2553
  tick(frame: FrameCallback): Unsubscribe;
2865
2554
  };
2866
2555
  //#endregion
2867
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/panel.d.ts
2556
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/panel.d.ts
2557
+ /**
2558
+ * The pages of the host's panel a game has a name for, as the keys of this
2559
+ * interface.
2560
+ *
2561
+ * An interface rather than a union so that a preview can add its own pages to
2562
+ * it, as it adds its own namespace to `GameSdk`: a page only a preview's games
2563
+ * are offered is named only where that preview is installed.
2564
+ *
2565
+ * @group Panels
2566
+ */
2567
+ interface PanelPages {
2568
+ /** What this game is: its title, its author, its description. */
2569
+ about: true;
2570
+ /** Who has scored what, on this game's boards. */
2571
+ scores: true;
2572
+ /** What has been unlocked, and how far along the rest is. */
2573
+ achievements: true;
2574
+ /** What this game is played with, and where a player rebinds it. */
2575
+ input: true;
2576
+ }
2577
+ /**
2578
+ * A page of the host's panel: one of `PanelPages`.
2579
+ *
2580
+ * It grows, and it only ever grows: a name means the same thing for ever,
2581
+ * because a game ships and is never updated.
2582
+ *
2583
+ * @group Panels
2584
+ */
2585
+ type PageId = keyof PanelPages;
2868
2586
  /**
2869
2587
  * Where the host's panel stands over the game.
2870
2588
  *
@@ -2973,7 +2691,7 @@ type Panel = {
2973
2691
  open(page: PageId): void;
2974
2692
  };
2975
2693
  //#endregion
2976
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/pause.d.ts
2694
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/pause.d.ts
2977
2695
  /**
2978
2696
  * The host has asked for a pause, or for the game to carry on.
2979
2697
  *
@@ -3034,7 +2752,7 @@ type Pause = {
3034
2752
  on(listener: PauseListener): Unsubscribe;
3035
2753
  };
3036
2754
  //#endregion
3037
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/resources.d.ts
2755
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/resources.d.ts
3038
2756
  /**
3039
2757
  * The host has let the game hold its resources again, or asked for them back.
3040
2758
  * Return a promise to make the host wait until they have gone, or come back.
@@ -3064,7 +2782,7 @@ type Resources = {
3064
2782
  on(listener: ResourcesListener): Unsubscribe;
3065
2783
  };
3066
2784
  //#endregion
3067
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/save.d.ts
2785
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/save.d.ts
3068
2786
  /**
3069
2787
  * What a game gets back from a read: its own schema number, and its state.
3070
2788
  *
@@ -3103,6 +2821,45 @@ type SaveStatus = {
3103
2821
  * @category Useful
3104
2822
  */
3105
2823
  type SaveListener = (now: SaveStatus, previous: SaveStatus) => void;
2824
+ /**
2825
+ * Make one save of two that went their own ways.
2826
+ *
2827
+ * `mine` is this device's, `theirs` one another device made meanwhile, and
2828
+ * neither was built on the other. Answer with the save to keep in place of
2829
+ * both, on whatever schema `version` it is written in. Throw where the two
2830
+ * cannot be merged — a schema this build does not know — and the host asks the
2831
+ * player which to keep.
2832
+ *
2833
+ * Pure: it is called with two states and must not touch the game's own. Run
2834
+ * while the game loads, its answer is what `read()` resolves with; run while
2835
+ * the game plays, its answer goes to `onMerge` as well.
2836
+ *
2837
+ * @group Save
2838
+ * @category Useful
2839
+ */
2840
+ type SaveMerger = (mine: SaveState, theirs: SaveState) => SaveState;
2841
+ /**
2842
+ * Told the merged save, once a merge has run while the game was playing on
2843
+ * its own: play on from this, which is both devices' progress.
2844
+ *
2845
+ * @group Save
2846
+ * @category Useful
2847
+ */
2848
+ type SaveMergeListener = (merged: SaveState) => void;
2849
+ /**
2850
+ * What a game says about its save at `init`, before the host has asked
2851
+ * anything.
2852
+ *
2853
+ * @group Save
2854
+ * @category Useful
2855
+ */
2856
+ type SaveSetup = {
2857
+ /**
2858
+ * How to make one save of two. Left out, the host asks the player which of
2859
+ * the two to keep. See `SaveMerger`.
2860
+ */
2861
+ merge?: SaveMerger | undefined;
2862
+ };
3106
2863
  /**
3107
2864
  * @group Save
3108
2865
  * @category Important
@@ -3149,11 +2906,20 @@ type Save = {
3149
2906
  }): Promise<void>;
3150
2907
  /** Throw the save away — a game resetting its own progress, at a player's asking. */
3151
2908
  clear(): Promise<void>;
2909
+ /**
2910
+ * Told when the game's `merge` ran while the game was playing: what the
2911
+ * host now keeps is the merged save, and the game should play on from it.
2912
+ *
2913
+ * A merge needed while the game plays waits for its next load unless
2914
+ * something is listening here, because a game that went on playing from its
2915
+ * own state would save over the merge with its next write.
2916
+ */
2917
+ onMerge(listener: SaveMergeListener): Unsubscribe;
3152
2918
  /** The largest save this host will take, in bytes. `SAVE_BYTES` where it said nothing. */
3153
2919
  readonly maxBytes: number;
3154
2920
  };
3155
2921
  //#endregion
3156
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/score.d.ts
2922
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/score.d.ts
3157
2923
  /**
3158
2924
  * Where this player stands on every board, as far as anybody has said.
3159
2925
  *
@@ -3230,7 +2996,7 @@ type Score<Board extends string = string> = {
3230
2996
  }): Promise<ScoreResult>;
3231
2997
  };
3232
2998
  //#endregion
3233
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/share.d.ts
2999
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/share.d.ts
3234
3000
  /**
3235
3001
  * @group Share
3236
3002
  * @category Important
@@ -3302,7 +3068,7 @@ type Share = {
3302
3068
  }): void;
3303
3069
  };
3304
3070
  //#endregion
3305
- //#region ../../../../../../../tmp/hopple-sdk-types-DAYaix/game/src/init.d.ts
3071
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/init.d.ts
3306
3072
  /**
3307
3073
  * What `init` hands back: every namespace a game has, on one object.
3308
3074
  *
@@ -3317,7 +3083,7 @@ type Share = {
3317
3083
  * @group Bridge
3318
3084
  * @category Useful
3319
3085
  */
3320
- type GameSdk<M extends object = GameManifest> = {
3086
+ interface GameSdk<M extends object = GameManifest> {
3321
3087
  /**
3322
3088
  * How far the game has got: `loading`, `ready`, `disposed`. The one thing
3323
3089
  * here a game must actually call is `lifecycle.ready()`.
@@ -3433,7 +3199,7 @@ type GameSdk<M extends object = GameManifest> = {
3433
3199
  *
3434
3200
  * @category Useful
3435
3201
  */
3436
- input: Input<ActionName<M>, LayoutName<M>>;
3202
+ input: Input<ActionName<M> | PlayerActionName<M>, LayoutName<M>, PlayerActionName<M>>;
3437
3203
  /**
3438
3204
  * How loud this game may be, which the host decides and the game obeys.
3439
3205
  *
@@ -3526,19 +3292,17 @@ type GameSdk<M extends object = GameManifest> = {
3526
3292
  *
3527
3293
  * Resolves with the round trip in milliseconds, rejecting with a `BridgeError`
3528
3294
  * 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.
3295
+ * check, and what a game can measure the bridge with.
3530
3296
  */
3531
3297
  ping(): Promise<number>;
3532
3298
  /** Tear the bridge down. Calls in flight reject with `disposed`. */
3533
3299
  dispose(): void;
3534
- };
3300
+ }
3535
3301
  /**
3536
3302
  * @group Bridge
3537
3303
  * @category Useful
3538
3304
  */
3539
3305
  type InitOptions = {
3540
- /** The window the game is running in. Defaults to the global one. */
3541
- scope?: Window | undefined;
3542
3306
  /**
3543
3307
  * How long a call waits before rejecting with `timeout`.
3544
3308
  *
@@ -3548,22 +3312,6 @@ type InitOptions = {
3548
3312
  * in it.
3549
3313
  */
3550
3314
  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
3315
  /**
3568
3316
  * Where a failure that belongs to nobody's promise goes — a lifecycle
3569
3317
  * listener that threw, an event the SDK refused. Defaults to `console.error`.
@@ -3572,6 +3320,15 @@ type InitOptions = {
3572
3320
  * them, as it is about any error the game throws and nothing catches.
3573
3321
  */
3574
3322
  onError?: ((error: unknown) => void) | undefined;
3323
+ /**
3324
+ * What the game says about its save before the host asks anything: how to
3325
+ * merge two saves that went their own ways, on two devices. See `SaveSetup`.
3326
+ *
3327
+ * Here rather than on `sdk.save`, because the host may ask while the game is
3328
+ * still reading its save — the merge is what that read resolves with — and a
3329
+ * merge set after the read would be one the host never heard of.
3330
+ */
3331
+ save?: SaveSetup | undefined;
3575
3332
  };
3576
3333
  /**
3577
3334
  * Introduce the game to its host, and hand back the SDK once it has answered.
@@ -3607,62 +3364,4 @@ type InitOptions = {
3607
3364
  */
3608
3365
  declare function init<M extends object = GameManifest>(options?: InitOptions): Promise<GameSdk<M>>;
3609
3366
  //#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 };
3367
+ 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 };