@hopple/sdk 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +729 -2
- package/config.schema.json +776 -0
- package/hopple-sdk.min.js +139 -0
- package/index.d.ts +3650 -0
- package/index.js +3548 -0
- package/manifest.md +281 -0
- package/package.json +30 -4
package/index.d.ts
ADDED
|
@@ -0,0 +1,3650 @@
|
|
|
1
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/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-1wJl1B/protocol/src/analytics.d.ts
|
|
32
|
+
/**
|
|
33
|
+
* What a game says happened.
|
|
34
|
+
*
|
|
35
|
+
* The bridge carries only this. Everything the host can see for itself — phases,
|
|
36
|
+
* placements, deadlines, the end — it records where the messages already are, so
|
|
37
|
+
* a game cannot skew it and a game written before any of this existed still
|
|
38
|
+
* leaves a full trace.
|
|
39
|
+
*
|
|
40
|
+
* **One event, one message, as it happens.** No buffer and no batch: a pipe
|
|
41
|
+
* sized for a handful of events a session gains nothing by holding them, and
|
|
42
|
+
* holding them would cost a flush on every way a game can end. Whoever delivers
|
|
43
|
+
* these to a server will batch there, where delivery is, and where the batching
|
|
44
|
+
* can be as clever as the delivery needs.
|
|
45
|
+
*
|
|
46
|
+
* **Nothing here is a limit.** How many events a run may carry, how long a name
|
|
47
|
+
* may be, how much a prop may hold: those are the host's, and every host will
|
|
48
|
+
* answer differently — a dev shell keeps everything, an app in the field keeps
|
|
49
|
+
* what its warehouse can take. What this file settles is the *shape*, which both
|
|
50
|
+
* ends must agree on, and the two rules the manifest is written under: what a
|
|
51
|
+
* name looks like, and the namespace a game may not use.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* What a prop may hold. Flat scalars, because a column is flat.
|
|
55
|
+
*
|
|
56
|
+
* @group Analytics
|
|
57
|
+
*/
|
|
58
|
+
type AnalyticsValue = string | number | boolean;
|
|
59
|
+
/** @group Analytics */
|
|
60
|
+
type AnalyticsProps = Record<string, AnalyticsValue>;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/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
|
+
*/
|
|
85
|
+
/**
|
|
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.
|
|
93
|
+
*
|
|
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.
|
|
101
|
+
*
|
|
102
|
+
* @group Audio
|
|
103
|
+
*/
|
|
104
|
+
declare const AUDIO_MASTER = "master";
|
|
105
|
+
/**
|
|
106
|
+
* How loud the game may be, as the host has it set right now.
|
|
107
|
+
*
|
|
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.
|
|
112
|
+
*
|
|
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
|
|
119
|
+
*/
|
|
120
|
+
type AudioState = {
|
|
121
|
+
/**
|
|
122
|
+
* Everything off, whatever the faders say.
|
|
123
|
+
*
|
|
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.
|
|
127
|
+
*/
|
|
128
|
+
muted: boolean;
|
|
129
|
+
/** The fader over all of it, `0`–`1`. */
|
|
130
|
+
master: number;
|
|
131
|
+
/**
|
|
132
|
+
* Each declared channel's own fader, `0`–`1`, by the id its manifest gave it.
|
|
133
|
+
*
|
|
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.
|
|
138
|
+
*/
|
|
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-1wJl1B/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
|
+
*/
|
|
180
|
+
/** @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';
|
|
222
|
+
/** @group Bridge */
|
|
223
|
+
type ProtocolError = {
|
|
224
|
+
code: ErrorCode;
|
|
225
|
+
message: string;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* What a rejected call rejects with.
|
|
229
|
+
*
|
|
230
|
+
* @group Bridge
|
|
231
|
+
*/
|
|
232
|
+
declare class BridgeError extends Error {
|
|
233
|
+
code: ErrorCode;
|
|
234
|
+
constructor(error: ProtocolError);
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/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-1wJl1B/protocol/src/input.d.ts
|
|
279
|
+
/**
|
|
280
|
+
* What the host says about how this game is being played.
|
|
281
|
+
*
|
|
282
|
+
* **Nothing about input is asked for over this bridge.** A key is pressed
|
|
283
|
+
* inside the frame and read inside the frame; a thumbstick under a thumb is
|
|
284
|
+
* sampled a metre from the game that wants it. Putting either on the wire would
|
|
285
|
+
* spend a message and a frame of latency on something already in the room.
|
|
286
|
+
*
|
|
287
|
+
* So what travels here is what the frame has no copy of. The game's own
|
|
288
|
+
* declaration — its actions and its layouts of on-screen controls — comes from
|
|
289
|
+
* the manifest, and the manifest is the host's: the frame holds none. With it
|
|
290
|
+
* comes the *mapping* — the declared bindings with whatever a player has
|
|
291
|
+
* remapped folded over them — the scheme they are using, and whether on-screen
|
|
292
|
+
* controls apply on this host at all. All of it is wanted before the first
|
|
293
|
+
* frame, and the last three can change under a running game without the game
|
|
294
|
+
* being able to notice.
|
|
295
|
+
*
|
|
296
|
+
* **The bindings are carried as strings and not parsed here.** Their vocabulary
|
|
297
|
+
* lives in `@hopple/manifest`, where a binding is first written down, and
|
|
298
|
+
* whoever resolves one imports it from there. A host newer than the game it is
|
|
299
|
+
* talking to will send bindings this game has never heard of, and the right
|
|
300
|
+
* thing to do with those is to keep them on the wire and drop them at the point
|
|
301
|
+
* of use — which is what a resolver that only matches what it knows does for
|
|
302
|
+
* free.
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Which kind of input the player is using right now.
|
|
306
|
+
*
|
|
307
|
+
* The last one they actually used, not everything attached: a laptop with a
|
|
308
|
+
* controller plugged in is a keyboard until somebody moves a stick. What a game
|
|
309
|
+
* does with it is choose its prompts — *Press Z* against a glyph — and decide
|
|
310
|
+
* whether to draw touch hints of its own.
|
|
311
|
+
*
|
|
312
|
+
* @group Input
|
|
313
|
+
*/
|
|
314
|
+
type InputScheme = 'keyboard' | 'mouse' | 'touch' | 'gamepad';
|
|
315
|
+
/** @group Input */
|
|
316
|
+
declare const INPUT_SCHEMES: readonly InputScheme[];
|
|
317
|
+
/**
|
|
318
|
+
* What presses one action, and what to call it in front of a player.
|
|
319
|
+
*
|
|
320
|
+
* `bind` is the effective list — the manifest's, with a remap folded over it —
|
|
321
|
+
* in the order a game should prefer them when it draws only one.
|
|
322
|
+
*
|
|
323
|
+
* **`label` is the host's, and it is why this is on the wire at all.** The same
|
|
324
|
+
* action is *Z* on a keyboard, *A* on one controller and *B* on another, and
|
|
325
|
+
* only the host knows which the player is holding. A game writes
|
|
326
|
+
* `Press ${mapping.shoot.label}` and is right everywhere without ever learning
|
|
327
|
+
* what a controller is. Absent means the host had nothing better to say than the
|
|
328
|
+
* game's own declaration.
|
|
329
|
+
*
|
|
330
|
+
* @group Input
|
|
331
|
+
*/
|
|
332
|
+
type InputBinding = {
|
|
333
|
+
bind: string[];
|
|
334
|
+
/** What to call the action itself. From the manifest unless the host knows better. */
|
|
335
|
+
title?: string;
|
|
336
|
+
/** What to show for the binding a player would actually use. See above. */
|
|
337
|
+
label?: string;
|
|
338
|
+
};
|
|
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 drawn 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. `fixed` stays where it is drawn and is moved only from there.
|
|
368
|
+
*
|
|
369
|
+
* @group Input
|
|
370
|
+
*/
|
|
371
|
+
type InputSticks = 'floating' | 'fixed';
|
|
372
|
+
/**
|
|
373
|
+
* Why the mapping or the scheme is different from what the game was told before.
|
|
374
|
+
*
|
|
375
|
+
* @group Input
|
|
376
|
+
*/
|
|
377
|
+
type InputChangeCause =
|
|
378
|
+
/** A player changed a binding. */
|
|
379
|
+
'remap' |
|
|
380
|
+
/** They picked up a controller, or put one down. */
|
|
381
|
+
'scheme' |
|
|
382
|
+
/** The host revised it for its own reasons. */
|
|
383
|
+
'host' |
|
|
384
|
+
/** A player changed how the on-screen controls behave. */
|
|
385
|
+
'setting';
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/panel.d.ts
|
|
388
|
+
/**
|
|
389
|
+
* The pages of the panel this version of the protocol has a name for.
|
|
390
|
+
*
|
|
391
|
+
* **A vocabulary, not a capability.** Every one of these is a subject both ends
|
|
392
|
+
* already have words for elsewhere — a game's own description, its boards, its
|
|
393
|
+
* achievements, its bindings — so the names are the ones the hosts already use
|
|
394
|
+
* for the pages they draw, and a game author who reads "About" in the panel
|
|
395
|
+
* finds `about` here.
|
|
396
|
+
*
|
|
397
|
+
* It grows, and it only ever grows: a name means the same thing for ever,
|
|
398
|
+
* because a game ships and is never updated. A host older than a name answers
|
|
399
|
+
* `unsupported`, which is the same thing it says about a page it simply does
|
|
400
|
+
* not draw — and the right answer to both, because from the game's side they
|
|
401
|
+
* are one fact: that page is not going to open here.
|
|
402
|
+
*
|
|
403
|
+
* @group Panels
|
|
404
|
+
*/
|
|
405
|
+
type PageId =
|
|
406
|
+
/** What this game is: its title, its author, its description. */
|
|
407
|
+
'about' |
|
|
408
|
+
/** Who has scored what, on this game's boards. */
|
|
409
|
+
'scores' |
|
|
410
|
+
/** What has been unlocked, and how far along the rest is. */
|
|
411
|
+
'achievements' |
|
|
412
|
+
/** What this game is played with, and where a player rebinds it. */
|
|
413
|
+
'input';
|
|
414
|
+
/** @group Panels */
|
|
415
|
+
declare const PAGES: readonly PageId[];
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/pause.d.ts
|
|
418
|
+
/**
|
|
419
|
+
* Whether the game is holding still, which the host may ask for and only the
|
|
420
|
+
* game can answer.
|
|
421
|
+
*
|
|
422
|
+
* **This is the third shape a namespace on this bridge takes, and it is worth
|
|
423
|
+
* naming.** A save, a score and a panel are services: the game asks and the
|
|
424
|
+
* host may refuse. Audio runs the other way: the host states and the game
|
|
425
|
+
* obeys, with no refusal to model. Pausing is neither. The host asks, and the
|
|
426
|
+
* game is the only side that knows whether there is anything to stop — so the
|
|
427
|
+
* answer is not an acknowledgement but a *state*, and one of the three it may
|
|
428
|
+
* be is "you cannot ask me that just now".
|
|
429
|
+
*
|
|
430
|
+
* **Pausing is not an order, and the two must not be folded together.** The
|
|
431
|
+
* orders in `Allowed` are what the *host* lets the game run — a loop stopped
|
|
432
|
+
* because the game is being swiped past is the host's fact, declared rather
|
|
433
|
+
* than negotiated. This is the player's fact: they pressed a button, and the
|
|
434
|
+
* game stopped its clock and put a menu up. A host asks for a pause as it takes
|
|
435
|
+
* a game in play away from the player, and leaves resuming to the player: one
|
|
436
|
+
* that muddled the two would un-pause a player's game by scrolling back to it.
|
|
437
|
+
*
|
|
438
|
+
* **Whether a game can be paused at all is nothing more than what it says.** A
|
|
439
|
+
* game that never announces a state is `unpausable` for ever and is never
|
|
440
|
+
* asked, which is what every game published before this existed gets. There is
|
|
441
|
+
* no separate capability to declare: "cannot be paused" and "nothing to stop
|
|
442
|
+
* just now" are one fact to a player, and one state here.
|
|
443
|
+
*
|
|
444
|
+
* **`unpausable` is a real state and not an error.** A game between rounds, on
|
|
445
|
+
* its title screen, or — in the demo — with nothing on the field to stop, has
|
|
446
|
+
* nothing to pause; saying so is more useful than refusing a call, because it
|
|
447
|
+
* is a fact the host draws rather than one it recovers from. It also moves on
|
|
448
|
+
* its own, which is why the game announces the state as well as answering
|
|
449
|
+
* about it.
|
|
450
|
+
*/
|
|
451
|
+
/**
|
|
452
|
+
* Where the game stands, as one word.
|
|
453
|
+
*
|
|
454
|
+
* Three values rather than a boolean and a capability flag, because the third
|
|
455
|
+
* one is what a host actually draws around: a control that is *absent* is a
|
|
456
|
+
* different thing from one that is showing "Pause", and a host holding two
|
|
457
|
+
* booleans has to work out which of their four combinations mean nothing.
|
|
458
|
+
*
|
|
459
|
+
* @group Pause
|
|
460
|
+
*/
|
|
461
|
+
type PauseState =
|
|
462
|
+
/** Playing. A host draws Pause. */
|
|
463
|
+
'running' |
|
|
464
|
+
/** Held, at the player's asking. A host draws Resume. */
|
|
465
|
+
'paused' |
|
|
466
|
+
/**
|
|
467
|
+
* There is nothing here to stop right now.
|
|
468
|
+
*
|
|
469
|
+
* A title screen, the gap between rounds, a cut scene that has to finish.
|
|
470
|
+
* A host draws nothing at all — not a disabled button, which is a promise
|
|
471
|
+
* that pressing it would do something.
|
|
472
|
+
*/
|
|
473
|
+
'unpausable';
|
|
474
|
+
/** @group Pause */
|
|
475
|
+
declare const PAUSE_STATES: readonly PauseState[];
|
|
476
|
+
/**
|
|
477
|
+
* The cautious reading, and the one every silence falls back to: a game that
|
|
478
|
+
* has said nothing cannot be paused.
|
|
479
|
+
*
|
|
480
|
+
* The opposite of the reading `AUDIO_FULL` takes, and for the same reason —
|
|
481
|
+
* both are the answer that claims the least. Guessing loud is safe for volume
|
|
482
|
+
* because a game that started silent would look broken; guessing `unpausable`
|
|
483
|
+
* is safe here because a host that drew Pause on a game with no pause would
|
|
484
|
+
* have promised a button that does nothing.
|
|
485
|
+
*
|
|
486
|
+
* @group Pause
|
|
487
|
+
*/
|
|
488
|
+
declare const PAUSE_NONE: PauseState;
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/save.d.ts
|
|
491
|
+
/**
|
|
492
|
+
* The one thing a game may keep.
|
|
493
|
+
*
|
|
494
|
+
* A game reads its state while it loads and writes it as it plays. Where that
|
|
495
|
+
* lands is the host's — `localStorage` in the dev shell today, a row behind an
|
|
496
|
+
* account later — and what is in it is the game's, which the host never parses.
|
|
497
|
+
*
|
|
498
|
+
* **The shape of this is not the shape of analytics**, and every difference is
|
|
499
|
+
* deliberate. Analytics is one-way and unacknowledged because nothing waits for
|
|
500
|
+
* it; a read's answer is the whole point, a write has to be known to have
|
|
501
|
+
* landed, and a save that quietly went missing is something a player feels. So
|
|
502
|
+
* these are requests, they have error codes, and the game is told when one
|
|
503
|
+
* failed.
|
|
504
|
+
*
|
|
505
|
+
* **Nothing here is a limit either.** How big a save a host will take, how often
|
|
506
|
+
* it will take one, whether it debounces and how long its window is: all the
|
|
507
|
+
* host's, all revisable on the host's own deploy cadence. What this file settles
|
|
508
|
+
* is the *shape* both ends must agree on, and one floor — `SAVE_BYTES` — that a
|
|
509
|
+
* game may assume anywhere, forever, because a game ships and is never updated.
|
|
510
|
+
*/
|
|
511
|
+
/**
|
|
512
|
+
* What a game may write anywhere, in bytes of `state` as UTF-8.
|
|
513
|
+
*
|
|
514
|
+
* The floor rather than the cap: a host that will take more says so in the
|
|
515
|
+
* welcome, and a game that never asks still knows this much is safe. 64 KiB is
|
|
516
|
+
* far more than a save of positions and counters needs and far less than any
|
|
517
|
+
* store would notice.
|
|
518
|
+
*
|
|
519
|
+
* @group Save
|
|
520
|
+
*/
|
|
521
|
+
declare const SAVE_BYTES = 65536;
|
|
522
|
+
/**
|
|
523
|
+
* What is waiting, without fetching it.
|
|
524
|
+
*
|
|
525
|
+
* Small, fixed-size, and already known to any host that has a store, so it
|
|
526
|
+
* rides in the `Welcome` where the blob deliberately does not. It is what tells
|
|
527
|
+
* a game whether the round trip is worth making: a game draws *Continue* or
|
|
528
|
+
* *New game* before it has asked for anything, and a game now on schema 3 that
|
|
529
|
+
* will not migrate a 1 never fetches it at all.
|
|
530
|
+
*
|
|
531
|
+
* @group Save
|
|
532
|
+
*/
|
|
533
|
+
type SaveStored = {
|
|
534
|
+
/** The host's clock, when the record was written. */
|
|
535
|
+
at: number;
|
|
536
|
+
version: number;
|
|
537
|
+
/** How big `state` is, so a game knows what it is about to be handed. */
|
|
538
|
+
bytes: number;
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/score.d.ts
|
|
542
|
+
/**
|
|
543
|
+
* What a player did, as one number.
|
|
544
|
+
*
|
|
545
|
+
* A game submits a score when a run ends; the host makes it durable and, if it
|
|
546
|
+
* has anywhere to send it, sends it. What comes back is what the player is
|
|
547
|
+
* shown: their own best, and where it stands.
|
|
548
|
+
*
|
|
549
|
+
* **The shape of this is not the shape of a save, and not the shape of
|
|
550
|
+
* analytics.** A save is one document rewritten in place, and losing one costs
|
|
551
|
+
* a player their progress. A score is an append — every run is its own fact, a
|
|
552
|
+
* later worse run does not erase a better one, and the collapsing into "your
|
|
553
|
+
* best" happens where the rows are rather than on the way in. Analytics is
|
|
554
|
+
* one-way because nothing waits for it; a score is a request because a game
|
|
555
|
+
* draws the answer.
|
|
556
|
+
*
|
|
557
|
+
* **The submission carries no player.** The game says which board and what
|
|
558
|
+
* number, and the host attaches who — which is the host being authoritative
|
|
559
|
+
* about the one fact a game must not be able to assert. It also means a game
|
|
560
|
+
* literally cannot submit on somebody else's behalf, which is free here and
|
|
561
|
+
* would be expensive to retrofit.
|
|
562
|
+
*
|
|
563
|
+
* **There is no anti-cheat and this file does not pretend otherwise.** A score
|
|
564
|
+
* is whatever the machine running the game says it is. What is checked is that
|
|
565
|
+
* the number is a number: finite, and inside the range where an integer still
|
|
566
|
+
* round-trips. The realistic thing that catches is an uninitialised variable.
|
|
567
|
+
*/
|
|
568
|
+
/**
|
|
569
|
+
* The largest score the wire will carry, and the smallest.
|
|
570
|
+
*
|
|
571
|
+
* Spelled here *and* in `@hopple/manifest`, on purpose and with a test to keep
|
|
572
|
+
* them equal. The server checks the same rule and is not allowed to depend on
|
|
573
|
+
* this package — "the server never talks to a game", made mechanical — so the
|
|
574
|
+
* rule is written down in the one package both sides may import and enforced on
|
|
575
|
+
* the wire here.
|
|
576
|
+
*
|
|
577
|
+
* @group Score
|
|
578
|
+
*/
|
|
579
|
+
declare const SCORE_VALUE_MAX = 9007199254740991;
|
|
580
|
+
/**
|
|
581
|
+
* What the host says came of it.
|
|
582
|
+
*
|
|
583
|
+
* `improved` and `best` are answerable by any host that took the submission,
|
|
584
|
+
* because they are about this player's own history and nothing else. `rank` and
|
|
585
|
+
* `of` are not: under a host that acknowledges when the score is *durable*
|
|
586
|
+
* rather than when a server has seen it, there may be no server answer yet. So
|
|
587
|
+
* they are optional, and a host that does not know leaves them out rather than
|
|
588
|
+
* guessing — a rank a game drew from a guess is a rank a player will screenshot.
|
|
589
|
+
*
|
|
590
|
+
* @group Score
|
|
591
|
+
*/
|
|
592
|
+
type ScoreResult = {
|
|
593
|
+
/** Whether this run beat the player's previous best. Ties are not improvements. */
|
|
594
|
+
improved: boolean;
|
|
595
|
+
/** The player's best on this board, after this submission. */
|
|
596
|
+
best: number;
|
|
597
|
+
/** Where that best stands, when the host knows. */
|
|
598
|
+
rank?: number;
|
|
599
|
+
/** Out of how many players, when the host knows. */
|
|
600
|
+
of?: number;
|
|
601
|
+
};
|
|
602
|
+
/**
|
|
603
|
+
* Where one player stands on one board.
|
|
604
|
+
*
|
|
605
|
+
* @group Score
|
|
606
|
+
*/
|
|
607
|
+
type ScoreStanding = {
|
|
608
|
+
best: number;
|
|
609
|
+
/** The host's clock, when the best was set. */
|
|
610
|
+
at: number;
|
|
611
|
+
/** How many runs this player has submitted to this board. */
|
|
612
|
+
runs: number;
|
|
613
|
+
rank?: number;
|
|
614
|
+
of?: number;
|
|
615
|
+
};
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/share.d.ts
|
|
618
|
+
/**
|
|
619
|
+
* A link to this game, offered to the player by the host.
|
|
620
|
+
*
|
|
621
|
+
* A game cannot share itself. It sits in a sandboxed frame on an opaque origin
|
|
622
|
+
* with no top-level navigation, no popups and no clipboard the player has
|
|
623
|
+
* gestured at — and it does not know its own address in the first place, because
|
|
624
|
+
* where this game lives is the host's fact and differs per host: a row in a feed
|
|
625
|
+
* on the site, a deep link into the app, a path on somebody else's page. So the
|
|
626
|
+
* game asks, and the host does the offering.
|
|
627
|
+
*
|
|
628
|
+
* **The game names no URL and is told none.** It says only what it would like
|
|
629
|
+
* carried along, and the host mints the link, decides how to put it in front of
|
|
630
|
+
* the player — a share sheet, the clipboard, a dialogue — and tells the player
|
|
631
|
+
* what became of it. A game that could read its own link would be a game that
|
|
632
|
+
* could fabricate one, and a host that took a URL from a game would be
|
|
633
|
+
* publishing whatever the game handed it.
|
|
634
|
+
*
|
|
635
|
+
* **The game is not told what the player did with it.** The host is the one
|
|
636
|
+
* that opened the sheet or wrote the clipboard, so the host is the one that
|
|
637
|
+
* says "link copied", and a game that forgot to would leave a player with a
|
|
638
|
+
* copy nobody mentioned. A game with an outcome in hand would also be a game
|
|
639
|
+
* that could pay for shares, which is a thing no player should be asked for.
|
|
640
|
+
*
|
|
641
|
+
* **`data` is the game's, and opaque to everyone else.** The host keeps it with
|
|
642
|
+
* the link and gives it back to whoever opens that link, without ever parsing
|
|
643
|
+
* it: a level seed, a board, a score to beat. What it means is settled between
|
|
644
|
+
* the game that wrote it and the same game reading it back, which is the only
|
|
645
|
+
* pair that can agree about it.
|
|
646
|
+
*
|
|
647
|
+
* **`meta` is the game's too, and it goes the other way: to the platform, never
|
|
648
|
+
* back to a game.** Why the link was made — the level, the score that was
|
|
649
|
+
* beaten, which of the game's own buttons was pressed — kept beside the host's
|
|
650
|
+
* own record of who made it and how, so the people running a game can learn
|
|
651
|
+
* which of its shares bring players in. The game reading a link back is never
|
|
652
|
+
* told it, so nothing a game plays on can depend on it.
|
|
653
|
+
*
|
|
654
|
+
* **A game asks; only the host refuses.** Nothing is negotiated at the
|
|
655
|
+
* handshake and there is nothing for a game to check first: whether there is a
|
|
656
|
+
* link to give, how often one may be asked for, how much data one will carry
|
|
657
|
+
* are all the host's, all revisable on the host's own deploy cadence, and all
|
|
658
|
+
* answered when a player actually presses the button. What this file settles is
|
|
659
|
+
* the shape both ends must agree on, and `SHARE_BYTES` — the amount of `data`
|
|
660
|
+
* every host takes, forever, because a game ships and is never updated.
|
|
661
|
+
*
|
|
662
|
+
* **Both directions, and they are not symmetrical.** Going out is a request
|
|
663
|
+
* the game does not wait on: the host answers as soon as it has taken the offer
|
|
664
|
+
* and before it opens anything, because a sheet stays up for as long as the
|
|
665
|
+
* player likes and no reply timeout can be long enough for that. Coming back —
|
|
666
|
+
* the data a link carried into this run — is a fact of the run and rides in the
|
|
667
|
+
* welcome beside `info`, because a game opened from a link has to know before
|
|
668
|
+
* its first frame. See `ShareWelcome`.
|
|
669
|
+
*/
|
|
670
|
+
/**
|
|
671
|
+
* What every host carries, in bytes of `data` as UTF-8.
|
|
672
|
+
*
|
|
673
|
+
* The floor rather than the cap: a host may take more and none may take less,
|
|
674
|
+
* so a game that stays under this is never refused for its size anywhere. It is
|
|
675
|
+
* the one number about sharing a game can rely on without asking, which is why
|
|
676
|
+
* it is a constant rather than something the wire carries.
|
|
677
|
+
*
|
|
678
|
+
* Small on purpose. 512 bytes is a seed, a board, a score; a save belongs in
|
|
679
|
+
* `save`, which is where a host keeps things that are big. It is the number
|
|
680
|
+
* every game already published was promised, so it stays the floor whether a
|
|
681
|
+
* host carries the data in the link itself or keeps it on a server and puts
|
|
682
|
+
* only a reference in the link.
|
|
683
|
+
*
|
|
684
|
+
* @group Share
|
|
685
|
+
*/
|
|
686
|
+
declare const SHARE_BYTES = 512;
|
|
687
|
+
/**
|
|
688
|
+
* What every host takes of `meta`, in bytes of its JSON as UTF-8.
|
|
689
|
+
*
|
|
690
|
+
* A floor like `SHARE_BYTES`, and for the same reason: a game that stays under
|
|
691
|
+
* it is never refused anywhere. Enough for a handful of facts about why a link
|
|
692
|
+
* was made; a game that wants to say more is describing its state, which is
|
|
693
|
+
* what `data` is for.
|
|
694
|
+
*
|
|
695
|
+
* @group Share
|
|
696
|
+
*/
|
|
697
|
+
declare const SHARE_META_BYTES = 1024;
|
|
698
|
+
/**
|
|
699
|
+
* Something a game can say about a share: what JSON can say, and nothing else.
|
|
700
|
+
*
|
|
701
|
+
* @group Share
|
|
702
|
+
*/
|
|
703
|
+
type ShareMetaValue = string | number | boolean | null | ShareMetaValue[] | {
|
|
704
|
+
[key: string]: ShareMetaValue;
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Why a game made a link, in its own words — kept by the platform beside who
|
|
708
|
+
* made it and how, and never handed back to a game.
|
|
709
|
+
*
|
|
710
|
+
* @group Share
|
|
711
|
+
*/
|
|
712
|
+
type ShareMeta = {
|
|
713
|
+
[key: string]: ShareMetaValue;
|
|
714
|
+
};
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/methods.d.ts
|
|
717
|
+
/**
|
|
718
|
+
* How far a game has got with its own preparation.
|
|
719
|
+
*
|
|
720
|
+
* Only ever forwards, and only ever about the game itself — not about what is
|
|
721
|
+
* being done with it. `disposed` ends it.
|
|
722
|
+
*
|
|
723
|
+
* booting → loading → ready → disposed
|
|
724
|
+
*
|
|
725
|
+
* @group Lifecycle
|
|
726
|
+
*/
|
|
727
|
+
type Phase =
|
|
728
|
+
/**
|
|
729
|
+
* The frame exists and nothing has spoken yet: the bundle is still starting
|
|
730
|
+
* up. A game never observes this — there is no game running to observe it —
|
|
731
|
+
* but a host needs a word for the gap, because a bundle that never runs looks
|
|
732
|
+
* exactly like one that has not run *yet*, and only a deadline tells them
|
|
733
|
+
* apart.
|
|
734
|
+
*/
|
|
735
|
+
'booting' |
|
|
736
|
+
/** Up and talking, getting its own things together: assets, save, first frame. */
|
|
737
|
+
'loading' |
|
|
738
|
+
/** The game says it can be shown. The one phase it declares rather than obeys. */
|
|
739
|
+
'ready' |
|
|
740
|
+
/** Going away. The last thing a game is told, and its chance to flush. */
|
|
741
|
+
'disposed';
|
|
742
|
+
/**
|
|
743
|
+
* A box in the frame's own coordinates: `0,0` is the frame's top-left corner.
|
|
744
|
+
*
|
|
745
|
+
* @group Layout
|
|
746
|
+
*/
|
|
747
|
+
type Rect = {
|
|
748
|
+
x: number;
|
|
749
|
+
y: number;
|
|
750
|
+
width: number;
|
|
751
|
+
height: number;
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* How big something is, in CSS pixels.
|
|
755
|
+
*
|
|
756
|
+
* @group Layout
|
|
757
|
+
*/
|
|
758
|
+
type Size = {
|
|
759
|
+
width: number;
|
|
760
|
+
height: number;
|
|
761
|
+
};
|
|
762
|
+
/**
|
|
763
|
+
* What is over the game, and why.
|
|
764
|
+
*
|
|
765
|
+
* A game avoids all of these the same way, so they arrive in one list rather
|
|
766
|
+
* than several a game would have to concatenate before it could use them.
|
|
767
|
+
*
|
|
768
|
+
* @group Layout
|
|
769
|
+
*/
|
|
770
|
+
type ObstructionKind =
|
|
771
|
+
/** Host interface: a rail, a corner control. It takes the input there too. */
|
|
772
|
+
'chrome' |
|
|
773
|
+
/** The device itself: a notch, a camera hole, a rounded corner. */
|
|
774
|
+
'cutout' |
|
|
775
|
+
/** Something the host has put over the game for now: a sheet, a dialogue. */
|
|
776
|
+
'overlay' |
|
|
777
|
+
/**
|
|
778
|
+
* The SDK's own on-screen controls, drawn inside the frame.
|
|
779
|
+
*
|
|
780
|
+
* The one kind a host never sends: it is synthesised where the controls are
|
|
781
|
+
* drawn, and joins the same list so that a game keeping clear of reserved
|
|
782
|
+
* rectangles keeps clear of a thumbstick without being told twice.
|
|
783
|
+
*/
|
|
784
|
+
'controls';
|
|
785
|
+
/** @group Layout */
|
|
786
|
+
type Obstruction = {
|
|
787
|
+
rect: Rect;
|
|
788
|
+
kind: ObstructionKind;
|
|
789
|
+
/**
|
|
790
|
+
* Whether this is part of the game's usable area or merely on top of it.
|
|
791
|
+
*
|
|
792
|
+
* `true` means stay out: the region is not yours, and pointer events there
|
|
793
|
+
* will never reach you. `false` means something is covering you for now and
|
|
794
|
+
* what to do about it is entirely your decision — the SDK leaves it out of
|
|
795
|
+
* the insets, so a game that ignores this keeps the layout it had.
|
|
796
|
+
*
|
|
797
|
+
* A separate field rather than something to be read off `kind`, because a
|
|
798
|
+
* game ships and is never updated: one written today will one day meet a kind
|
|
799
|
+
* nobody has invented yet, and this is what lets it be right about it anyway.
|
|
800
|
+
*/
|
|
801
|
+
reserved: boolean;
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* Which kind of host this is.
|
|
805
|
+
*
|
|
806
|
+
* @group Info
|
|
807
|
+
*/
|
|
808
|
+
type AppSurface =
|
|
809
|
+
/** The Hopple app, in a WebView. */
|
|
810
|
+
'app' |
|
|
811
|
+
/** A browser: the production site, or a game embedded on someone else's page. */
|
|
812
|
+
'web' |
|
|
813
|
+
/** A developer's own machine, under `hopple dev`. Never a player. */
|
|
814
|
+
'dev';
|
|
815
|
+
/**
|
|
816
|
+
* How much machine there is, as one word.
|
|
817
|
+
*
|
|
818
|
+
* A budget the host worked out when it started the game, not a live reading —
|
|
819
|
+
* which is what makes it safe to say once. Anything that changes while a game
|
|
820
|
+
* runs is a different kind of fact and would need somewhere to change.
|
|
821
|
+
*
|
|
822
|
+
* @group Info
|
|
823
|
+
*/
|
|
824
|
+
type DeviceTier = 'low' | 'mid' | 'high';
|
|
825
|
+
/**
|
|
826
|
+
* Which host is running this game.
|
|
827
|
+
*
|
|
828
|
+
* @group Info
|
|
829
|
+
*/
|
|
830
|
+
type AppInfo = {
|
|
831
|
+
/** Who it is, as a stable name: `hopple`, or whoever embedded the game. */
|
|
832
|
+
id: string;
|
|
833
|
+
/** Their version. For a game's own diagnostics; the protocol version is what capability is settled by. */
|
|
834
|
+
version: string;
|
|
835
|
+
/**
|
|
836
|
+
* `dev` is the one worth acting on, and the one to be careful with: it is how
|
|
837
|
+
* a game shows its own frame counter to its author and to nobody else. A game
|
|
838
|
+
* that *plays* differently in dev is a game whose dev testing proves less than
|
|
839
|
+
* it looks.
|
|
840
|
+
*/
|
|
841
|
+
surface: AppSurface;
|
|
842
|
+
};
|
|
843
|
+
/**
|
|
844
|
+
* Which game the host believes it is running.
|
|
845
|
+
*
|
|
846
|
+
* From the manifest that arrived with the verified bundle, not from anything the
|
|
847
|
+
* bundle says about itself. A game that inlined its own version at build time
|
|
848
|
+
* and finds it disagreeing with this one has been shipped wrong, and from inside
|
|
849
|
+
* the frame there is nowhere else that is visible.
|
|
850
|
+
*
|
|
851
|
+
* @group Info
|
|
852
|
+
*/
|
|
853
|
+
type GameInfo = {
|
|
854
|
+
id: string;
|
|
855
|
+
version: string;
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Who is playing, as far as a game is allowed to know.
|
|
859
|
+
*
|
|
860
|
+
* @group Info
|
|
861
|
+
*/
|
|
862
|
+
type PlayerInfo = {
|
|
863
|
+
/**
|
|
864
|
+
* An opaque name for this player *in this game*, stable while they keep
|
|
865
|
+
* playing it, and `null` where the host has no notion of a player at all —
|
|
866
|
+
* a site running one game has none, and a game keying anything to a player
|
|
867
|
+
* has to survive that.
|
|
868
|
+
*
|
|
869
|
+
* **Per game, not per person.** A host derives this from the player and the
|
|
870
|
+
* game together, so two games by the same author cannot recognise the same
|
|
871
|
+
* person between them. Nothing here can enforce that — this package only
|
|
872
|
+
* carries what a host hands over — so it is a rule every host we write obeys.
|
|
873
|
+
*/
|
|
874
|
+
id: string | null;
|
|
875
|
+
/** Whether this player has played *this* game before. */
|
|
876
|
+
returning: boolean;
|
|
877
|
+
/**
|
|
878
|
+
* The language to speak, as a BCP 47 tag.
|
|
879
|
+
*
|
|
880
|
+
* The player's, not the device's, which is why it is here rather than under
|
|
881
|
+
* `device`: `navigator.language` is already readable inside the frame and
|
|
882
|
+
* answers what the operating system is set to, while this is what they chose
|
|
883
|
+
* in the app.
|
|
884
|
+
*/
|
|
885
|
+
locale: string;
|
|
886
|
+
};
|
|
887
|
+
/**
|
|
888
|
+
* What the host is running the game on.
|
|
889
|
+
*
|
|
890
|
+
* @group Info
|
|
891
|
+
*/
|
|
892
|
+
type DeviceInfo = {
|
|
893
|
+
tier: DeviceTier;
|
|
894
|
+
};
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/listeners.d.ts
|
|
897
|
+
/**
|
|
898
|
+
* The one way anything here is listened to: `on(listener)` hands back the
|
|
899
|
+
* function that stops it.
|
|
900
|
+
*
|
|
901
|
+
* Every `on()` a game or a host is offered behaves the same, because they are
|
|
902
|
+
* all this:
|
|
903
|
+
*
|
|
904
|
+
* - **Each `on()` is its own.** The same function added twice is called twice,
|
|
905
|
+
* and each returned function takes back only the registration it came from —
|
|
906
|
+
* so two parts of a game that happen to share a handler cannot unsubscribe
|
|
907
|
+
* each other.
|
|
908
|
+
* - **Stopping is final and safe to repeat.** A listener taken back while
|
|
909
|
+
* others are being told is not called after that point, and calling the
|
|
910
|
+
* returned function again does nothing.
|
|
911
|
+
* - **A listener added while others are being told hears the next change**,
|
|
912
|
+
* not the one under way: it was not there when it happened.
|
|
913
|
+
* - **One that throws is reported and stops nobody.** The others are still
|
|
914
|
+
* told, and whoever made the change still gets on with it.
|
|
915
|
+
*
|
|
916
|
+
* @module listeners
|
|
917
|
+
*/
|
|
918
|
+
/**
|
|
919
|
+
* Stops the listener an `on()` added. Calling it again does nothing.
|
|
920
|
+
*
|
|
921
|
+
* @group Bridge
|
|
922
|
+
*/
|
|
923
|
+
type Unsubscribe = () => void;
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/protocol/src/transport.d.ts
|
|
926
|
+
/** @group Bridge */
|
|
927
|
+
type Transport = {
|
|
928
|
+
send(message: Envelope): void;
|
|
929
|
+
/** Returns the unsubscribe. Calling `receive` twice is not defined behaviour. */
|
|
930
|
+
receive(handler: (message: Envelope) => void): () => void;
|
|
931
|
+
close(): void;
|
|
932
|
+
};
|
|
933
|
+
/**
|
|
934
|
+
* The name a platform leaves its shim under, and the name it calls if it is late.
|
|
935
|
+
*
|
|
936
|
+
* Declared here, in the package both ends import, so that a platform author and
|
|
937
|
+
* the SDK are reading the same two strings. Nothing in this package writes
|
|
938
|
+
* either of them: the shim is wholly the platform's, and this file is a target
|
|
939
|
+
* to hit rather than code we ship.
|
|
940
|
+
*
|
|
941
|
+
* @group Bridge
|
|
942
|
+
*/
|
|
943
|
+
declare const SHIM = "__HOPPLE_SHIM__";
|
|
944
|
+
/** @group Bridge */
|
|
945
|
+
declare const SET_SHIM = "__HOPPLE_SET_SHIM__";
|
|
946
|
+
/**
|
|
947
|
+
* What a native shell leaves for a game to talk through.
|
|
948
|
+
*
|
|
949
|
+
* A game in a browser frame has `window.parent` and needs none of this. A game
|
|
950
|
+
* in a WebView is a top-level document — its `parent` is itself — so there is no
|
|
951
|
+
* window above it to post to, and the platform has to hand it a channel instead.
|
|
952
|
+
* This is that channel, reduced to the least a platform can be asked for.
|
|
953
|
+
*
|
|
954
|
+
* **The SDK writes none of it.** It agrees on a name and a shape and finds
|
|
955
|
+
* whatever was left; how the bytes actually cross is the platform's business,
|
|
956
|
+
* and is different on every one. That is what keeps this package free of hosts
|
|
957
|
+
* it has never heard of.
|
|
958
|
+
*
|
|
959
|
+
* A shim has exactly two duties:
|
|
960
|
+
*
|
|
961
|
+
* - **Arrive.** Before the game's bundle runs is easiest, and is what our own
|
|
962
|
+
* hosts do — a document-start hook, `injectedJavaScriptBeforeContentLoaded`,
|
|
963
|
+
* a `WKUserScript` at `.atDocumentStart`. A platform that cannot manage that
|
|
964
|
+
* is late rather than broken: see the rendezvous below.
|
|
965
|
+
* - **Never throw.** A shim that throws breaks a game that did nothing wrong.
|
|
966
|
+
*
|
|
967
|
+
* And one duty it explicitly does *not* have: **it must not buffer.** Nothing
|
|
968
|
+
* arrives before the game is listening, because every host → game message is
|
|
969
|
+
* gated on the handshake and the game is the side that opens it. An array in
|
|
970
|
+
* every shim would be paying, on every platform, for a message that is never
|
|
971
|
+
* sent.
|
|
972
|
+
*
|
|
973
|
+
* **Two ways in, and a platform may use either**, because neither side may
|
|
974
|
+
* assume it is first:
|
|
975
|
+
*
|
|
976
|
+
* ```js
|
|
977
|
+
* // The whole of what a platform has to get right about ordering.
|
|
978
|
+
* if (w.__HOPPLE_SET_SHIM__) w.__HOPPLE_SET_SHIM__(shim);
|
|
979
|
+
* else w.__HOPPLE_SHIM__ = shim;
|
|
980
|
+
* ```
|
|
981
|
+
*
|
|
982
|
+
* A platform that installs early always takes the second branch; one that
|
|
983
|
+
* cannot takes the first, and the SDK — which has been waiting — connects. The
|
|
984
|
+
* setter only exists while the SDK is waiting, and is removed once it is used
|
|
985
|
+
* or the wait expires.
|
|
986
|
+
*
|
|
987
|
+
* **Strings, not envelopes.** Every native bridge carries strings, so that is
|
|
988
|
+
* the contract, and it means the SDK stays the trust boundary: `JSON.parse` and
|
|
989
|
+
* `isEnvelope` run in SDK code on both sides of the wire and never in a
|
|
990
|
+
* platform's. A shim cannot get validation wrong, because it does none.
|
|
991
|
+
*
|
|
992
|
+
* @group Bridge
|
|
993
|
+
*/
|
|
994
|
+
type HoppleShim = {
|
|
995
|
+
/**
|
|
996
|
+
* The contract version. `1` today.
|
|
997
|
+
*
|
|
998
|
+
* A shim whose version the SDK does not know is ignored rather than guessed
|
|
999
|
+
* at — which falls through to the frame branch, and then to the wait.
|
|
1000
|
+
*/
|
|
1001
|
+
v: 1;
|
|
1002
|
+
/** Send one already-serialised envelope to the host. Game → host. */
|
|
1003
|
+
send(message: string): void;
|
|
1004
|
+
/**
|
|
1005
|
+
* Take the SDK's inbound handler, and hand back the unsubscribe.
|
|
1006
|
+
*
|
|
1007
|
+
* Called once, by the SDK, and it is a subscription rather than a delivery:
|
|
1008
|
+
* how the platform later reaches the handler it was given — an eval into a
|
|
1009
|
+
* global, a real message channel — is the platform's problem and is not
|
|
1010
|
+
* described here.
|
|
1011
|
+
*/
|
|
1012
|
+
receive(handler: (message: string) => void): () => void;
|
|
1013
|
+
};
|
|
1014
|
+
/**
|
|
1015
|
+
* The two globals, so a platform's own TypeScript sees the same shape the SDK
|
|
1016
|
+
* reads. Neither is ever written by this package.
|
|
1017
|
+
*/
|
|
1018
|
+
declare global {
|
|
1019
|
+
var __HOPPLE_SHIM__: HoppleShim | undefined;
|
|
1020
|
+
var __HOPPLE_SET_SHIM__: ((shim: HoppleShim) => void) | undefined;
|
|
1021
|
+
}
|
|
1022
|
+
//#endregion
|
|
1023
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/manifest/src/manifest.d.ts
|
|
1024
|
+
/**
|
|
1025
|
+
* What a game says about itself.
|
|
1026
|
+
*
|
|
1027
|
+
* Written in the project's hopple config and uploaded beside the game's page
|
|
1028
|
+
* as `game.manifest.json`. It is the first thing the host learns: it is
|
|
1029
|
+
* published with the game, so a host holds it before it has anything to run —
|
|
1030
|
+
* from the server in the app, from the project in the dev server. It never
|
|
1031
|
+
* travels over the bridge. Not to be confused with `FeedEntry`, which is
|
|
1032
|
+
* what the *catalogue* says about a game.
|
|
1033
|
+
*
|
|
1034
|
+
* The core is deliberately tiny: identity, a name, and how the game wants to
|
|
1035
|
+
* be held. Everything a later API needs declared ahead of time
|
|
1036
|
+
* gets its own optional top-level key named after that API — `save`,
|
|
1037
|
+
* `achievements`, and so on — whose contents are designed by the milestone that
|
|
1038
|
+
* builds it. Most namespaces will never need one.
|
|
1039
|
+
*
|
|
1040
|
+
* **The types below are the only place a rule is written.** Their shapes and
|
|
1041
|
+
* the constraint tags in their comments — `@maxLength`, `@pattern` and the rest
|
|
1042
|
+
* — are compiled into `game.manifest.schema.json`, and `validateManifest` checks
|
|
1043
|
+
* a manifest by walking that schema. Change a type and the schema, the
|
|
1044
|
+
* validator and every editor that reads the schema follow. The only rules
|
|
1045
|
+
* written in code are the few no schema can state, and they are at the bottom
|
|
1046
|
+
* of this file.
|
|
1047
|
+
*
|
|
1048
|
+
* Validation lives here rather than in the host so that the publish CLI, the
|
|
1049
|
+
* dev server and the host all reach the same verdict about the same file.
|
|
1050
|
+
*
|
|
1051
|
+
* @module manifest
|
|
1052
|
+
*/
|
|
1053
|
+
/**
|
|
1054
|
+
* The name a manifest is uploaded under, beside the game's page.
|
|
1055
|
+
*
|
|
1056
|
+
* @group Manifest
|
|
1057
|
+
* @category Useful
|
|
1058
|
+
*/
|
|
1059
|
+
declare const MANIFEST_FILE = "game.manifest.json";
|
|
1060
|
+
/**
|
|
1061
|
+
* The manifest's own version, separate from the protocol's.
|
|
1062
|
+
*
|
|
1063
|
+
* Bumped only if the *core* changes meaning, which is a thing we intend never
|
|
1064
|
+
* to do — new keys are additive and need no bump. A host refuses a schema it
|
|
1065
|
+
* does not know, because a manifest it half-understands is worse than none.
|
|
1066
|
+
*
|
|
1067
|
+
* @group Manifest
|
|
1068
|
+
* @category Useful
|
|
1069
|
+
*/
|
|
1070
|
+
declare const MANIFEST_SCHEMA = 1;
|
|
1071
|
+
/**
|
|
1072
|
+
* How the game wants to be held. The host decides what to do about it.
|
|
1073
|
+
*
|
|
1074
|
+
* @group Layout
|
|
1075
|
+
*/
|
|
1076
|
+
type Orientation = 'portrait' | 'landscape' | 'any';
|
|
1077
|
+
/**
|
|
1078
|
+
* Whether the host may put its own furniture over the game's frame.
|
|
1079
|
+
*
|
|
1080
|
+
* The one layout question that has to be answered before the bundle runs, which
|
|
1081
|
+
* is why it is declared here rather than asked at runtime: the host sizes the
|
|
1082
|
+
* frame before there is a game to ask. A game that says nothing gets `reserve`,
|
|
1083
|
+
* so a rail never lands on the buttons of something written before rails
|
|
1084
|
+
* existed.
|
|
1085
|
+
*
|
|
1086
|
+
* @group Layout
|
|
1087
|
+
*/
|
|
1088
|
+
type InsetMode =
|
|
1089
|
+
/** The frame is the whole surface; read `sdk.layout` and keep out of the way. */
|
|
1090
|
+
'draw-under' |
|
|
1091
|
+
/** The host shrinks the frame clear of its own furniture. Nothing to avoid. */
|
|
1092
|
+
'reserve';
|
|
1093
|
+
/**
|
|
1094
|
+
* What a game declares about layout. Absent means every default.
|
|
1095
|
+
*
|
|
1096
|
+
* @group Layout
|
|
1097
|
+
* @category Useful
|
|
1098
|
+
*/
|
|
1099
|
+
type LayoutDeclaration = {
|
|
1100
|
+
insets: InsetMode;
|
|
1101
|
+
[key: string]: unknown;
|
|
1102
|
+
};
|
|
1103
|
+
/**
|
|
1104
|
+
* What a prop may hold, as one word. The three the wire carries, and no more.
|
|
1105
|
+
*
|
|
1106
|
+
* @group Analytics
|
|
1107
|
+
*/
|
|
1108
|
+
type AnalyticsType = 'string' | 'number' | 'boolean';
|
|
1109
|
+
/**
|
|
1110
|
+
* One event's props. Every one optional: see `AnalyticsDeclaration`.
|
|
1111
|
+
*
|
|
1112
|
+
* Prop names follow the grammar event names do, which `@hopple/protocol`
|
|
1113
|
+
* enforces on the wire too; a test keeps the two in step.
|
|
1114
|
+
*
|
|
1115
|
+
* @group Analytics
|
|
1116
|
+
* @maxProperties 8
|
|
1117
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 24, errorMessage: 'must be a prop name of lowercase letters, digits, dots, hyphens and underscores' }
|
|
1118
|
+
*/
|
|
1119
|
+
type AnalyticsShape = Record<string, AnalyticsType>;
|
|
1120
|
+
/**
|
|
1121
|
+
* The custom events a game sends, and what each one carries.
|
|
1122
|
+
*
|
|
1123
|
+
* **The first key here whose reader is not the host.** `layout.insets` is
|
|
1124
|
+
* declared because a frame has to be sized before there is a game to ask;
|
|
1125
|
+
* nothing about analytics has to be answered before the bundle runs. What earns
|
|
1126
|
+
* this its place is cardinality: names and columns invented at runtime make a
|
|
1127
|
+
* pipe nobody can build a table for, and the party that has to build that table
|
|
1128
|
+
* — the publish CLI, the server, a creator dashboard — is exactly the party that
|
|
1129
|
+
* already reads the manifest and never talks to a game.
|
|
1130
|
+
*
|
|
1131
|
+
* **Every prop is optional and nothing is required**, which is what keeps the
|
|
1132
|
+
* declaration cheap enough to be worth asking for: forgetting one costs a column
|
|
1133
|
+
* rather than the event. The additive rule carries over unchanged — add events,
|
|
1134
|
+
* add props, never retype one — and that is the whole of what a per-event table
|
|
1135
|
+
* needs to survive the game's next version.
|
|
1136
|
+
*
|
|
1137
|
+
* @group Analytics
|
|
1138
|
+
* @category Useful
|
|
1139
|
+
*/
|
|
1140
|
+
type AnalyticsDeclaration = {
|
|
1141
|
+
/**
|
|
1142
|
+
* Every event the game may send, by name.
|
|
1143
|
+
*
|
|
1144
|
+
* Names starting `hopple.` are the host's own records and not a game's to
|
|
1145
|
+
* use. The grammar is the wire's too: `@hopple/protocol` enforces the same
|
|
1146
|
+
* one, and a test keeps the two in step. At most 64 events: the wire never
|
|
1147
|
+
* sees a declaration, so nothing enforces this at runtime and nothing has to.
|
|
1148
|
+
*
|
|
1149
|
+
* @maxProperties 64
|
|
1150
|
+
* @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' }
|
|
1151
|
+
*/
|
|
1152
|
+
events: Record<string, AnalyticsShape>;
|
|
1153
|
+
[key: string]: unknown;
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* What a game expects to keep, in bytes.
|
|
1157
|
+
*
|
|
1158
|
+
* **The manifest's own test is whether the host must know before the bundle
|
|
1159
|
+
* runs**, and here it must: a host that prefetches a save from a server has to
|
|
1160
|
+
* know which games have one before there is a game to ask, and a publish
|
|
1161
|
+
* pipeline that provisions storage has to know before a player exists.
|
|
1162
|
+
*
|
|
1163
|
+
* The budget the game expects, not a grant. A host answers with its own cap at
|
|
1164
|
+
* the handshake, and a game with no key here may still call the API — it just
|
|
1165
|
+
* gets nothing prefetched, which is the honest reading of not having said so.
|
|
1166
|
+
*
|
|
1167
|
+
* @group Save
|
|
1168
|
+
* @category Useful
|
|
1169
|
+
*/
|
|
1170
|
+
type SaveDeclaration = {
|
|
1171
|
+
/**
|
|
1172
|
+
* At most 1 MiB. Not the same question as what a host will take — that is
|
|
1173
|
+
* the host's and is settled at the handshake. This is only what is worth
|
|
1174
|
+
* writing down: a declaration in the megabytes is a game that has
|
|
1175
|
+
* misunderstood what this is for, and finding that out at publish time is
|
|
1176
|
+
* cheaper than at a player's.
|
|
1177
|
+
*
|
|
1178
|
+
* @asType integer
|
|
1179
|
+
* @exclusiveMinimum 0
|
|
1180
|
+
* @maximum 1048576
|
|
1181
|
+
*/
|
|
1182
|
+
bytes: number;
|
|
1183
|
+
[key: string]: unknown;
|
|
1184
|
+
};
|
|
1185
|
+
/**
|
|
1186
|
+
* One thing a game makes noise with, as the player is offered it.
|
|
1187
|
+
*
|
|
1188
|
+
* The title is the whole of it, and required: this exists to be a label on a
|
|
1189
|
+
* fader in a host's own panel, and a fader labelled `sfx2` is a control nobody
|
|
1190
|
+
* can use. What a channel *is* — a bus, a set of samples, one oscillator — is
|
|
1191
|
+
* the game's business and is never described here.
|
|
1192
|
+
*
|
|
1193
|
+
* @group Audio
|
|
1194
|
+
*/
|
|
1195
|
+
type AudioChannel = {
|
|
1196
|
+
/**
|
|
1197
|
+
* What to call it in front of a player.
|
|
1198
|
+
*
|
|
1199
|
+
* @category Important
|
|
1200
|
+
* @pattern \S
|
|
1201
|
+
* @maxLength 40
|
|
1202
|
+
* @errorMessage must not be blank
|
|
1203
|
+
*/
|
|
1204
|
+
title: string;
|
|
1205
|
+
[key: string]: unknown;
|
|
1206
|
+
};
|
|
1207
|
+
/**
|
|
1208
|
+
* What this game makes noise with, so a host can offer a fader for each.
|
|
1209
|
+
*
|
|
1210
|
+
* **A map from the first version, even for a game with one channel.** The
|
|
1211
|
+
* channel id travels in `audio.set`, which is frozen game-side code, and a
|
|
1212
|
+
* single-channel key would have to *change meaning* to grow a second one — the
|
|
1213
|
+
* one move the additive rule forbids.
|
|
1214
|
+
*
|
|
1215
|
+
* **The manifest's test is whether the host must know before the bundle runs**,
|
|
1216
|
+
* and here it must twice over. The panel is drawn where the game's title is,
|
|
1217
|
+
* which a host draws for games in a feed that nobody has opened; and the
|
|
1218
|
+
* opening volumes ride in the handshake, so the host has to know which channels
|
|
1219
|
+
* it is stating a volume for before there is a game to ask.
|
|
1220
|
+
*
|
|
1221
|
+
* **The key is required, and `null` is how a silent game says so.** Leaving it
|
|
1222
|
+
* out would read the same as forgetting it, and a game that makes noise but
|
|
1223
|
+
* never said so is one whose player cannot turn it down. An object with no
|
|
1224
|
+
* channels is a game with sound and no tracks worth a fader of their own: it
|
|
1225
|
+
* gets a master fader and a mute button and no more. What is never optional is
|
|
1226
|
+
* obeying what the host then says: see `AudioState`.
|
|
1227
|
+
*
|
|
1228
|
+
* @group Audio
|
|
1229
|
+
* @category Useful
|
|
1230
|
+
*/
|
|
1231
|
+
type AudioDeclaration = {
|
|
1232
|
+
/**
|
|
1233
|
+
* The channels, by id. Optional, and may be empty: either way there is only
|
|
1234
|
+
* the master. At most 8: a panel of faders nobody can read is not a feature.
|
|
1235
|
+
*
|
|
1236
|
+
* `master` is refused rather than quietly shadowed. A master fader exists
|
|
1237
|
+
* whether or not a game has said anything, and an author who wrote it meant
|
|
1238
|
+
* something by it; a fader that turned out to be the host's own would be a
|
|
1239
|
+
* control that ignored half of what they wired it to. The id grammar is the
|
|
1240
|
+
* wire's too, and a test keeps the two in step.
|
|
1241
|
+
*
|
|
1242
|
+
* @maxProperties 8
|
|
1243
|
+
* @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' }
|
|
1244
|
+
*/
|
|
1245
|
+
channels?: Record<string, AudioChannel>;
|
|
1246
|
+
[key: string]: unknown;
|
|
1247
|
+
};
|
|
1248
|
+
/**
|
|
1249
|
+
* Which way a board sorts — the one thing about a score that cannot be guessed.
|
|
1250
|
+
*
|
|
1251
|
+
* `desc` is the ordinary leaderboard, where more is better. `asc` is a time, a
|
|
1252
|
+
* stroke count, a number of moves. Nothing downstream can infer this from the
|
|
1253
|
+
* values it holds, and a client that guessed would render a time board upside
|
|
1254
|
+
* down, so it is required rather than defaulted.
|
|
1255
|
+
*
|
|
1256
|
+
* @group Score
|
|
1257
|
+
*/
|
|
1258
|
+
type ScoreDirection = 'asc' | 'desc';
|
|
1259
|
+
/**
|
|
1260
|
+
* How to draw the number.
|
|
1261
|
+
*
|
|
1262
|
+
* A client renders a board for a game it has never run, so it cannot ask the
|
|
1263
|
+
* game what `224310` means. `integer` is a count, `decimal` a measurement,
|
|
1264
|
+
* `duration-ms` a time to be written as minutes and seconds. Absent is
|
|
1265
|
+
* `integer`, which is what most boards are.
|
|
1266
|
+
*
|
|
1267
|
+
* @group Score
|
|
1268
|
+
*/
|
|
1269
|
+
type ScoreFormat = 'integer' | 'decimal' | 'duration-ms';
|
|
1270
|
+
/** @group Score */
|
|
1271
|
+
type ScoreBoard = {
|
|
1272
|
+
/**
|
|
1273
|
+
* What to call it in front of a player.
|
|
1274
|
+
*
|
|
1275
|
+
* @category Important
|
|
1276
|
+
* @pattern \S
|
|
1277
|
+
* @maxLength 40
|
|
1278
|
+
* @errorMessage must not be blank
|
|
1279
|
+
*/
|
|
1280
|
+
title: string;
|
|
1281
|
+
direction: ScoreDirection;
|
|
1282
|
+
/** See `ScoreFormat`. Absent means `integer`. */
|
|
1283
|
+
format?: ScoreFormat;
|
|
1284
|
+
[key: string]: unknown;
|
|
1285
|
+
};
|
|
1286
|
+
/**
|
|
1287
|
+
* The boards this game keeps a score on.
|
|
1288
|
+
*
|
|
1289
|
+
* **A map from the first version, even for a game that wants one board.** The
|
|
1290
|
+
* board id travels in `score.submit`, which is frozen game-side code, and a
|
|
1291
|
+
* single-board key would have to *change meaning* to grow a second one — the
|
|
1292
|
+
* one thing the additive rule forbids. A map costs a game with one board the
|
|
1293
|
+
* few characters of naming it, and costs a game with four nothing at all.
|
|
1294
|
+
*
|
|
1295
|
+
* **Read by the host and by the server**, which is what earns it a key rather
|
|
1296
|
+
* than a runtime call: a client draws a game's board in a sheet over a feed
|
|
1297
|
+
* without ever having run that game, and the server has to know how a board
|
|
1298
|
+
* sorts before it can answer a query about one.
|
|
1299
|
+
*
|
|
1300
|
+
* @group Score
|
|
1301
|
+
* @category Useful
|
|
1302
|
+
*/
|
|
1303
|
+
type ScoreDeclaration = {
|
|
1304
|
+
/**
|
|
1305
|
+
* The boards, by id. At least one: a `score` key with no boards is a game
|
|
1306
|
+
* that meant to declare one and did not, and reading it as "no boards" would
|
|
1307
|
+
* hand it a scoreboard tab with nothing behind it. The id grammar is the
|
|
1308
|
+
* wire's too, and a test keeps the two in step.
|
|
1309
|
+
*
|
|
1310
|
+
* @minProperties 1
|
|
1311
|
+
* @maxProperties 16
|
|
1312
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
1313
|
+
*/
|
|
1314
|
+
boards: Record<string, ScoreBoard>;
|
|
1315
|
+
[key: string]: unknown;
|
|
1316
|
+
};
|
|
1317
|
+
/**
|
|
1318
|
+
* One named step on the way to a measured achievement's target.
|
|
1319
|
+
*
|
|
1320
|
+
* **A tier is a label, never a second achievement.** The achievement is
|
|
1321
|
+
* unlocked at its `target` and nowhere else; a tier is what a panel calls the
|
|
1322
|
+
* player between here and there — *Silver, 50 of 100*. Modelling each tier as
|
|
1323
|
+
* its own id would be three rows in every board that draws them and three
|
|
1324
|
+
* unlocks a game has to remember to send, where this is one number reported
|
|
1325
|
+
* once and read three ways.
|
|
1326
|
+
*
|
|
1327
|
+
* @group Achievements
|
|
1328
|
+
*/
|
|
1329
|
+
type AchievementTier = {
|
|
1330
|
+
/**
|
|
1331
|
+
* The reported value this tier is reached at, on the near side of the target.
|
|
1332
|
+
*
|
|
1333
|
+
* @minimum -9007199254740991
|
|
1334
|
+
* @maximum 9007199254740991
|
|
1335
|
+
*/
|
|
1336
|
+
at: number;
|
|
1337
|
+
/**
|
|
1338
|
+
* What to call the player who has reached it.
|
|
1339
|
+
*
|
|
1340
|
+
* @category Important
|
|
1341
|
+
* @pattern \S
|
|
1342
|
+
* @maxLength 40
|
|
1343
|
+
* @errorMessage must not be blank
|
|
1344
|
+
*/
|
|
1345
|
+
title: string;
|
|
1346
|
+
/**
|
|
1347
|
+
* A glyph for this tier, where it wants its own.
|
|
1348
|
+
*
|
|
1349
|
+
* @pattern \S
|
|
1350
|
+
* @maxLength 8
|
|
1351
|
+
* @errorMessage must not be blank
|
|
1352
|
+
*/
|
|
1353
|
+
icon?: string;
|
|
1354
|
+
[key: string]: unknown;
|
|
1355
|
+
};
|
|
1356
|
+
/**
|
|
1357
|
+
* What makes an achievement measured rather than flagged.
|
|
1358
|
+
*
|
|
1359
|
+
* **`direction` is the one thing that cannot be guessed**, exactly as it cannot
|
|
1360
|
+
* be guessed for a board. `desc` — the default, and what most achievements are
|
|
1361
|
+
* — counts up to the target: *break 100 blocks*. `asc` counts down to it:
|
|
1362
|
+
* *finish under two minutes*, where the value kept is the player's best time
|
|
1363
|
+
* and a smaller one beats a larger. A panel that assumed bigger-is-better would
|
|
1364
|
+
* draw a stopwatch achievement as though every slow run were progress.
|
|
1365
|
+
*
|
|
1366
|
+
* **`format` is how to draw the number**, for the reason a board needs one:
|
|
1367
|
+
* whoever renders this has never run the game and cannot ask it what `112400`
|
|
1368
|
+
* means.
|
|
1369
|
+
*
|
|
1370
|
+
* Both are `ScoreDirection` and `ScoreFormat` rather than types of their own.
|
|
1371
|
+
* "Which way is better" and "how is this number written" are the same two
|
|
1372
|
+
* questions wherever they are asked, and a second spelling of each is two
|
|
1373
|
+
* vocabularies to keep in step and one bug when they drift.
|
|
1374
|
+
*
|
|
1375
|
+
* @group Achievements
|
|
1376
|
+
*/
|
|
1377
|
+
type AchievementProgress = {
|
|
1378
|
+
/**
|
|
1379
|
+
* The value that unlocks it. Reaching it is the whole of being unlocked.
|
|
1380
|
+
*
|
|
1381
|
+
* Bounded by `Number.MAX_SAFE_INTEGER`, for the reason `SCORE_VALUE_MAX` is:
|
|
1382
|
+
* past there a whole number stops round-tripping through JSON and through a
|
|
1383
|
+
* 64-bit column, and progress toward a target nobody can reproduce is
|
|
1384
|
+
* progress nobody can draw.
|
|
1385
|
+
*
|
|
1386
|
+
* @minimum -9007199254740991
|
|
1387
|
+
* @maximum 9007199254740991
|
|
1388
|
+
*/
|
|
1389
|
+
target: number;
|
|
1390
|
+
/** Which way is better. Absent is `desc`: more is better. */
|
|
1391
|
+
direction?: ScoreDirection;
|
|
1392
|
+
/** How to draw the value. Absent is `integer`. */
|
|
1393
|
+
format?: ScoreFormat;
|
|
1394
|
+
/**
|
|
1395
|
+
* Named milestones on the way, ordered toward the target.
|
|
1396
|
+
*
|
|
1397
|
+
* @maxItems 8
|
|
1398
|
+
*/
|
|
1399
|
+
tiers?: AchievementTier[];
|
|
1400
|
+
[key: string]: unknown;
|
|
1401
|
+
};
|
|
1402
|
+
/**
|
|
1403
|
+
* One thing a player can be shown to have done.
|
|
1404
|
+
*
|
|
1405
|
+
* **Two kinds, told apart by one key.** Without `progress` this is a flag: it
|
|
1406
|
+
* happens once, and saying so twice is the same fact twice. With `progress` it
|
|
1407
|
+
* is measured — a count, a distance, a best time — reported as the game goes
|
|
1408
|
+
* and unlocked when the value reaches the target.
|
|
1409
|
+
*
|
|
1410
|
+
* **`secret` withholds the words, never the row.** A secret achievement is
|
|
1411
|
+
* listed while it is locked, so a player can see there is one left, with its
|
|
1412
|
+
* title and detail held back until it is theirs. Hiding the row itself would
|
|
1413
|
+
* make the list's length change under the player, which reads as a bug rather
|
|
1414
|
+
* than as a surprise. It is a courtesy of the panel and not a secret kept from
|
|
1415
|
+
* the machine: the manifest ships beside the bundle and is public, so anybody
|
|
1416
|
+
* who wants the words already has them.
|
|
1417
|
+
*
|
|
1418
|
+
* @group Achievements
|
|
1419
|
+
*/
|
|
1420
|
+
type Achievement = {
|
|
1421
|
+
/**
|
|
1422
|
+
* What to call it in front of a player.
|
|
1423
|
+
*
|
|
1424
|
+
* @category Important
|
|
1425
|
+
* @pattern \S
|
|
1426
|
+
* @maxLength 40
|
|
1427
|
+
* @errorMessage must not be blank
|
|
1428
|
+
*/
|
|
1429
|
+
title: string;
|
|
1430
|
+
/**
|
|
1431
|
+
* One line under the title, saying what it takes.
|
|
1432
|
+
*
|
|
1433
|
+
* @pattern \S
|
|
1434
|
+
* @maxLength 120
|
|
1435
|
+
* @errorMessage must not be blank
|
|
1436
|
+
*/
|
|
1437
|
+
detail?: string;
|
|
1438
|
+
/**
|
|
1439
|
+
* A short glyph to draw beside it — an emoji, usually. Eight characters is
|
|
1440
|
+
* enough for an emoji with a modifier on it.
|
|
1441
|
+
*
|
|
1442
|
+
* @pattern \S
|
|
1443
|
+
* @maxLength 8
|
|
1444
|
+
* @errorMessage must not be blank
|
|
1445
|
+
*/
|
|
1446
|
+
icon?: string;
|
|
1447
|
+
/** Whether the words are withheld until it is unlocked. See above. */
|
|
1448
|
+
secret?: boolean;
|
|
1449
|
+
/** Present makes this measured rather than flagged. See `AchievementProgress`. */
|
|
1450
|
+
progress?: AchievementProgress;
|
|
1451
|
+
[key: string]: unknown;
|
|
1452
|
+
};
|
|
1453
|
+
/**
|
|
1454
|
+
* The achievements this game keeps for a player.
|
|
1455
|
+
*
|
|
1456
|
+
* **A map, for the reason `score.boards` is one**: the id travels in
|
|
1457
|
+
* `achievement.unlock`, which is frozen game-side code, so the shape has to
|
|
1458
|
+
* hold a second entry without anything changing meaning.
|
|
1459
|
+
*
|
|
1460
|
+
* **Read by the host, by the client and by the server, and by none of them from
|
|
1461
|
+
* the game.** The site draws this panel for a game sitting in the feed that has
|
|
1462
|
+
* never been run; the server refuses an id no publisher declared. That is what
|
|
1463
|
+
* earns a manifest key rather than a runtime call — the same test `score`
|
|
1464
|
+
* passed.
|
|
1465
|
+
*
|
|
1466
|
+
* @group Achievements
|
|
1467
|
+
* @category Useful
|
|
1468
|
+
*/
|
|
1469
|
+
type AchievementDeclaration = {
|
|
1470
|
+
/**
|
|
1471
|
+
* The achievements, by id. At least one: an `achievements` key with nothing
|
|
1472
|
+
* in it is a game that meant to declare one and did not. The id grammar is
|
|
1473
|
+
* the wire's too, and a test keeps the two in step.
|
|
1474
|
+
*
|
|
1475
|
+
* @minProperties 1
|
|
1476
|
+
* @maxProperties 128
|
|
1477
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
1478
|
+
*/
|
|
1479
|
+
list: Record<string, Achievement>;
|
|
1480
|
+
[key: string]: unknown;
|
|
1481
|
+
};
|
|
1482
|
+
/**
|
|
1483
|
+
* A device a game can be played with, as one word.
|
|
1484
|
+
*
|
|
1485
|
+
* Hardware rather than API surface: `mouse` and `touch` are separate words even
|
|
1486
|
+
* though a game reads both through one pointer, because what a *client* does
|
|
1487
|
+
* with this is decide whether to put a game in front of somebody who has no way
|
|
1488
|
+
* to play it.
|
|
1489
|
+
*
|
|
1490
|
+
* @group Input
|
|
1491
|
+
*/
|
|
1492
|
+
type InputDevice = 'keyboard' | 'mouse' | 'touch' | 'gamepad';
|
|
1493
|
+
/**
|
|
1494
|
+
* One set of devices that together are enough to play.
|
|
1495
|
+
*
|
|
1496
|
+
* @group Input
|
|
1497
|
+
* @minItems 1
|
|
1498
|
+
* @maxItems 8
|
|
1499
|
+
*/
|
|
1500
|
+
type InputDeviceSet = InputDevice[];
|
|
1501
|
+
/**
|
|
1502
|
+
* A name for something that can *press* an action.
|
|
1503
|
+
*
|
|
1504
|
+
* `key.<code>` names a physical key by `KeyboardEvent.code` — **the code and
|
|
1505
|
+
* never the character**. `KeyA` is the key where A sits on a US keyboard and
|
|
1506
|
+
* stays that key on AZERTY, where it types Q, so a game that binds movement to
|
|
1507
|
+
* codes has WASD under the same four fingers everywhere. `Escape` is the host's:
|
|
1508
|
+
* it leaves fullscreen and pointer lock, which are invisible from inside the
|
|
1509
|
+
* frame, and a game that bound it would lose that fight quietly on every press.
|
|
1510
|
+
*
|
|
1511
|
+
* `pad.south` is the south of either cluster: the face button on one pad and the
|
|
1512
|
+
* d-pad's down on another, which is what makes one binding cover both. Compass
|
|
1513
|
+
* words for both clusters, with no `pad.dpad.down` alias beside
|
|
1514
|
+
* `pad.left.south` — a vocabulary that can never shrink should not start out
|
|
1515
|
+
* doubled. A thumbstick is not one of these; see `InputStick`.
|
|
1516
|
+
*
|
|
1517
|
+
* @group Input
|
|
1518
|
+
* @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\.(?:primary|secondary)|mouse\.(?:left|middle|right)|touch\.tap|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))$
|
|
1519
|
+
* @errorMessage is not a binding this SDK knows (a thumbstick is read rather than bound, and Escape is the host's key)
|
|
1520
|
+
*/
|
|
1521
|
+
type InputBinding$1 = string;
|
|
1522
|
+
/**
|
|
1523
|
+
* A name for a thumbstick — something a game reads, never binds.
|
|
1524
|
+
*
|
|
1525
|
+
* @group Input
|
|
1526
|
+
* @pattern ^(?:controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.stick|pad\.(?:left|right)\.stick)$
|
|
1527
|
+
* @errorMessage must name a thumbstick, such as pad.left.stick
|
|
1528
|
+
*/
|
|
1529
|
+
type InputStick = string;
|
|
1530
|
+
/**
|
|
1531
|
+
* What presses an action: at least one binding, and at most 8.
|
|
1532
|
+
*
|
|
1533
|
+
* @group Input
|
|
1534
|
+
* @minItems 1
|
|
1535
|
+
* @maxItems 8
|
|
1536
|
+
*/
|
|
1537
|
+
type InputBindings = InputBinding$1[];
|
|
1538
|
+
/**
|
|
1539
|
+
* One thing a player does, named by the game and bound to whatever produces it.
|
|
1540
|
+
*
|
|
1541
|
+
* **An action is a button and never an axis.** A key, a face button on a pad, a
|
|
1542
|
+
* pointer button and an on-screen button all press the same action, because
|
|
1543
|
+
* from the game's side they are the same event. A thumbstick is not one of
|
|
1544
|
+
* those and is read through its own API — folding the two together would put a
|
|
1545
|
+
* range on every binding that mostly does not have one, and would leave a game
|
|
1546
|
+
* unable to say whether it meant the press or the lean.
|
|
1547
|
+
*
|
|
1548
|
+
* **An object rather than a bare list of bindings**, from the first version,
|
|
1549
|
+
* because two strings are wanted the moment anybody draws this: a `title` for
|
|
1550
|
+
* the remapping screen, which shows actions to a player and cannot show
|
|
1551
|
+
* `move.left`, and a `label` for the face of an on-screen button. Growing a
|
|
1552
|
+
* list into an object later would change what an action's value *is*, which is
|
|
1553
|
+
* the one move the additive rule forbids. Only the object is accepted, even for
|
|
1554
|
+
* an action that needs neither string: one way to write an action is one way to
|
|
1555
|
+
* read it.
|
|
1556
|
+
*
|
|
1557
|
+
* @group Input
|
|
1558
|
+
*/
|
|
1559
|
+
type InputAction = {
|
|
1560
|
+
/** What presses it. See `InputBinding` for the vocabulary. */
|
|
1561
|
+
bind: InputBindings;
|
|
1562
|
+
/**
|
|
1563
|
+
* What to call it in front of a player, in a remapping screen.
|
|
1564
|
+
*
|
|
1565
|
+
* @pattern \S
|
|
1566
|
+
* @maxLength 40
|
|
1567
|
+
* @errorMessage must not be blank
|
|
1568
|
+
*/
|
|
1569
|
+
title?: string;
|
|
1570
|
+
/**
|
|
1571
|
+
* What to draw on an on-screen button: a glyph, or a letter.
|
|
1572
|
+
*
|
|
1573
|
+
* @pattern \S
|
|
1574
|
+
* @maxLength 8
|
|
1575
|
+
* @errorMessage must not be blank
|
|
1576
|
+
*/
|
|
1577
|
+
label?: string;
|
|
1578
|
+
[key: string]: unknown;
|
|
1579
|
+
};
|
|
1580
|
+
/**
|
|
1581
|
+
* Where on the screen a group of on-screen controls sits.
|
|
1582
|
+
*
|
|
1583
|
+
* @group Input
|
|
1584
|
+
*/
|
|
1585
|
+
type ControlZone = 'left' | 'right' | 'full';
|
|
1586
|
+
/**
|
|
1587
|
+
* How a group of on-screen buttons is arranged.
|
|
1588
|
+
*
|
|
1589
|
+
* Arrangement only, with the count doing the rest, so three across and five
|
|
1590
|
+
* down need no words of their own. **A shape nobody recognises is drawn as a
|
|
1591
|
+
* `column`** — the same escape `Obstruction.reserved` is. Whoever renders these
|
|
1592
|
+
* may be older than the game that declared them, and a control that fails to
|
|
1593
|
+
* appear is a game that cannot be played at all. It is still refused at
|
|
1594
|
+
* publish, where saying so is cheap.
|
|
1595
|
+
*
|
|
1596
|
+
* @group Input
|
|
1597
|
+
*/
|
|
1598
|
+
type ControlShape = 'row' | 'column' | 'diamond' | 'cluster';
|
|
1599
|
+
/**
|
|
1600
|
+
* One on-screen control: a group of buttons, or a thumbstick.
|
|
1601
|
+
*
|
|
1602
|
+
* **A button slot names an action; a stick slot names a stick.** The asymmetry
|
|
1603
|
+
* is the hardware's. On-screen buttons have no fixed count or position — three
|
|
1604
|
+
* across, five down, four in a diamond — so there is no standard control to
|
|
1605
|
+
* unify them with, and the action is the only stable name they have. Thumbsticks
|
|
1606
|
+
* come in twos and everybody agrees which is which, so a virtual one says which
|
|
1607
|
+
* it stands for and a game reads the physical stick and this one through one id.
|
|
1608
|
+
*
|
|
1609
|
+
* There is no d-pad here. A d-pad is four buttons wherever it exists — four
|
|
1610
|
+
* entries on the wire from a real pad, four keys on a keyboard — so a layout
|
|
1611
|
+
* that wants one declares four buttons and binds them like anything else.
|
|
1612
|
+
*
|
|
1613
|
+
* @group Input
|
|
1614
|
+
* @if { properties: { type: { const: 'buttons' } }, required: ['type'] }
|
|
1615
|
+
* @then { required: ['buttons'], properties: { as: { type: 'object', additionalProperties: { $ref: '#/definitions/InputBinding' } }, axis: { not: {}, errorMessage: 'is only for a thumbstick' } } }
|
|
1616
|
+
* @else { properties: { as: { $ref: '#/definitions/InputStick' } } }
|
|
1617
|
+
*/
|
|
1618
|
+
type ControlSlot = {
|
|
1619
|
+
type: 'buttons' | 'thumbstick';
|
|
1620
|
+
/**
|
|
1621
|
+
* `buttons` only: the actions this control presses, in drawing order.
|
|
1622
|
+
*
|
|
1623
|
+
* @minItems 1
|
|
1624
|
+
* @maxItems 8
|
|
1625
|
+
*/
|
|
1626
|
+
buttons?: string[];
|
|
1627
|
+
/** `buttons` only. See `ControlShape`. Absent is `column`. */
|
|
1628
|
+
shape?: ControlShape;
|
|
1629
|
+
/**
|
|
1630
|
+
* What this control stands in for on a standard pad.
|
|
1631
|
+
*
|
|
1632
|
+
* On a thumbstick it is the stick id a game reads — `pad.left.stick` — so one
|
|
1633
|
+
* `onStick` sees the physical stick and this one alike. Left out, the stick is
|
|
1634
|
+
* only itself, which a game reads as `controls.<id>`. On a group of buttons it
|
|
1635
|
+
* is a map from action to standard button, optional and rarely wanted: a game
|
|
1636
|
+
* whose bindings already name `pad.south` needs nothing here, and this is for
|
|
1637
|
+
* one that also wants the low-level path to see the press.
|
|
1638
|
+
*/
|
|
1639
|
+
as?: InputStick | Record<string, InputBinding$1>;
|
|
1640
|
+
/** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
|
|
1641
|
+
axis?: ControlAxis;
|
|
1642
|
+
[key: string]: unknown;
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* The one direction an on-screen thumbstick moves in: `x` across, `y` up and
|
|
1646
|
+
* down.
|
|
1647
|
+
*
|
|
1648
|
+
* For a game that steers along one line — a runner sliding left and right, a
|
|
1649
|
+
* paddle — where a stick that also leans up and down is a thumb wandering off
|
|
1650
|
+
* the line it means. The drawn stick becomes a track along that axis and always
|
|
1651
|
+
* reports the other one as 0. **Only the drawn stick:** a real pad's stick still
|
|
1652
|
+
* reports both, so a game reads the axis it wants from either.
|
|
1653
|
+
*
|
|
1654
|
+
* Whoever draws the controls may be older than the game, and draw a stick that
|
|
1655
|
+
* moves both ways. The SDK zeroes the other axis whatever the renderer reports,
|
|
1656
|
+
* so the game hears one axis either way.
|
|
1657
|
+
*
|
|
1658
|
+
* @group Input
|
|
1659
|
+
*/
|
|
1660
|
+
type ControlAxis = 'x' | 'y';
|
|
1661
|
+
/**
|
|
1662
|
+
* The controls in one zone of a layout, by id.
|
|
1663
|
+
*
|
|
1664
|
+
* @group Input
|
|
1665
|
+
* @maxProperties 8
|
|
1666
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a control id of lowercase letters, digits, dots, hyphens and underscores' }
|
|
1667
|
+
*/
|
|
1668
|
+
type ControlZoneSlots = Record<string, ControlSlot>;
|
|
1669
|
+
/**
|
|
1670
|
+
* One named arrangement of on-screen controls.
|
|
1671
|
+
*
|
|
1672
|
+
* A game may declare several and switch between them — a menu wants different
|
|
1673
|
+
* controls from play, and a driving section different ones again — which is why
|
|
1674
|
+
* they are named rather than there being one. A zone nobody knows is a layout
|
|
1675
|
+
* from a newer SDK, which the additive rule says must still load, so whoever
|
|
1676
|
+
* draws it simply has nowhere to put it.
|
|
1677
|
+
*
|
|
1678
|
+
* @group Input
|
|
1679
|
+
*/
|
|
1680
|
+
type ControlLayout = {
|
|
1681
|
+
left?: ControlZoneSlots;
|
|
1682
|
+
right?: ControlZoneSlots;
|
|
1683
|
+
full?: ControlZoneSlots;
|
|
1684
|
+
[key: string]: unknown;
|
|
1685
|
+
};
|
|
1686
|
+
/**
|
|
1687
|
+
* What a game says about how it is played.
|
|
1688
|
+
*
|
|
1689
|
+
* **It passes the manifest's test twice.** The host must know before the bundle
|
|
1690
|
+
* runs — whether on-screen controls apply at all, and what a remapping screen
|
|
1691
|
+
* offers, are settled before there is a game to ask — and it is read by parties
|
|
1692
|
+
* that never run the game: a feed deciding whether to show a keyboard-only game
|
|
1693
|
+
* to somebody holding a phone, and a remapping screen opened from outside a run.
|
|
1694
|
+
*
|
|
1695
|
+
* Action, control and layout ids follow the grammar the wire does, and a test
|
|
1696
|
+
* keeps the two in step.
|
|
1697
|
+
*
|
|
1698
|
+
* @group Input
|
|
1699
|
+
* @category Useful
|
|
1700
|
+
*/
|
|
1701
|
+
type InputDeclaration = {
|
|
1702
|
+
/**
|
|
1703
|
+
* The sets of hardware this game can be played with: any one set, every
|
|
1704
|
+
* member of which must be present.
|
|
1705
|
+
*
|
|
1706
|
+
* A flat list of alternatives rather than a tree of `any` and `all`. Every
|
|
1707
|
+
* real answer is a handful of alternatives, and this one renders straight into
|
|
1708
|
+
* *needs a keyboard and a mouse* without anybody having written a solver.
|
|
1709
|
+
*
|
|
1710
|
+
* **The publisher's claim, taken at its word**, exactly as `orientation` is:
|
|
1711
|
+
* nothing here checks it against the bindings. It cannot be checked. A game is
|
|
1712
|
+
* free to read the platform directly — raw key events, an accelerometer, a
|
|
1713
|
+
* microphone — and that is a stated allowance rather than an oversight, so
|
|
1714
|
+
* what a game *binds* never implies what it can be played with.
|
|
1715
|
+
*
|
|
1716
|
+
* @minItems 1
|
|
1717
|
+
* @maxItems 8
|
|
1718
|
+
*/
|
|
1719
|
+
supported?: InputDeviceSet[];
|
|
1720
|
+
/**
|
|
1721
|
+
* The actions, by id.
|
|
1722
|
+
*
|
|
1723
|
+
* @maxProperties 64
|
|
1724
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
1725
|
+
*/
|
|
1726
|
+
actions?: Record<string, InputAction>;
|
|
1727
|
+
/**
|
|
1728
|
+
* Named arrangements of on-screen controls. See `ControlLayout`.
|
|
1729
|
+
*
|
|
1730
|
+
* @maxProperties 8
|
|
1731
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a layout name of lowercase letters, digits, dots, hyphens and underscores' }
|
|
1732
|
+
*/
|
|
1733
|
+
controls?: Record<string, ControlLayout>;
|
|
1734
|
+
[key: string]: unknown;
|
|
1735
|
+
};
|
|
1736
|
+
/**
|
|
1737
|
+
* The index signature is the additive-only rule applied to this file: a game
|
|
1738
|
+
* built against a newer SDK declares keys an older host has never heard of, and
|
|
1739
|
+
* must still run there. It costs a typo in a manifest literal going unnoticed
|
|
1740
|
+
* by the compiler, which the validator catches instead.
|
|
1741
|
+
*
|
|
1742
|
+
* @group Manifest
|
|
1743
|
+
* @category Important
|
|
1744
|
+
*/
|
|
1745
|
+
type GameManifest = {
|
|
1746
|
+
/** @category Important */
|
|
1747
|
+
schema: typeof MANIFEST_SCHEMA;
|
|
1748
|
+
/**
|
|
1749
|
+
* Stable across versions, and unique among its publisher's games: it is the
|
|
1750
|
+
* second half of the address the game is published at, `owner/id`, and the
|
|
1751
|
+
* name every new version is filed under. Lowercase, url-safe, and no two
|
|
1752
|
+
* hyphens in a row, because it ends up in urls and storage keys.
|
|
1753
|
+
*
|
|
1754
|
+
* @category Important
|
|
1755
|
+
* @pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$
|
|
1756
|
+
* @maxLength 64
|
|
1757
|
+
* @errorMessage must be lowercase letters, digits and hyphens
|
|
1758
|
+
*/
|
|
1759
|
+
id: string;
|
|
1760
|
+
/**
|
|
1761
|
+
* The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
|
|
1762
|
+
*
|
|
1763
|
+
* @category Important
|
|
1764
|
+
* @pattern ^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
|
|
1765
|
+
* @errorMessage must look like 1.0.0
|
|
1766
|
+
*/
|
|
1767
|
+
version: string;
|
|
1768
|
+
/**
|
|
1769
|
+
* @category Important
|
|
1770
|
+
* @pattern \S
|
|
1771
|
+
* @maxLength 60
|
|
1772
|
+
* @errorMessage must not be blank
|
|
1773
|
+
*/
|
|
1774
|
+
title: string;
|
|
1775
|
+
/**
|
|
1776
|
+
* A sentence or two about the game, shown beside its title.
|
|
1777
|
+
*
|
|
1778
|
+
* @category Useful
|
|
1779
|
+
* @pattern \S
|
|
1780
|
+
* @maxLength 500
|
|
1781
|
+
* @errorMessage must not be blank
|
|
1782
|
+
*/
|
|
1783
|
+
description?: string;
|
|
1784
|
+
/**
|
|
1785
|
+
* A picture of the game, shown wherever a link to it is pasted — a chat, a
|
|
1786
|
+
* post, a message.
|
|
1787
|
+
*
|
|
1788
|
+
* The path of a PNG, JPEG or WebP file, relative to the config the manifest
|
|
1789
|
+
* is in, and at most `COVER_MAX_BYTES`. `hopple publish` uploads it beside
|
|
1790
|
+
* the game. 1200 × 630 is the shape every platform shows whole; without one,
|
|
1791
|
+
* a link shows Hopple's own card.
|
|
1792
|
+
*
|
|
1793
|
+
* @category Useful
|
|
1794
|
+
* @pattern ^(?!/)(?!.*\.\.)[A-Za-z0-9._/-]+\.(?:png|jpe?g|webp)$
|
|
1795
|
+
* @maxLength 200
|
|
1796
|
+
* @errorMessage must be the path of a .png, .jpg or .webp file beside the config, like "cover.png"
|
|
1797
|
+
*/
|
|
1798
|
+
cover?: string;
|
|
1799
|
+
/** @category Important */
|
|
1800
|
+
orientation: Orientation;
|
|
1801
|
+
/**
|
|
1802
|
+
* The `@hopple/sdk` version the game was built against, optionally prefixed
|
|
1803
|
+
* with `^` or `~`.
|
|
1804
|
+
*
|
|
1805
|
+
* Recorded rather than enforced: what the host and the game actually have to
|
|
1806
|
+
* agree on is the protocol version, which the handshake settles. This is for
|
|
1807
|
+
* diagnostics — "every game that broke was built against 2.3" is a question
|
|
1808
|
+
* worth being able to ask. Deliberately far narrower than everything npm
|
|
1809
|
+
* accepts: a grammar we can read in one line is a grammar three packages can
|
|
1810
|
+
* agree on.
|
|
1811
|
+
*
|
|
1812
|
+
* @category Important
|
|
1813
|
+
* @pattern ^[\^~]?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
|
|
1814
|
+
* @errorMessage must be a version like 1.0.0, ^1.0.0 or ~1.0.0
|
|
1815
|
+
*/
|
|
1816
|
+
sdk: string;
|
|
1817
|
+
/**
|
|
1818
|
+
* How the game wants the host's furniture handled. See `InsetMode`.
|
|
1819
|
+
*
|
|
1820
|
+
* @category Useful
|
|
1821
|
+
*/
|
|
1822
|
+
layout?: LayoutDeclaration;
|
|
1823
|
+
/**
|
|
1824
|
+
* The custom events this game sends. See `AnalyticsDeclaration`.
|
|
1825
|
+
*
|
|
1826
|
+
* @category Useful
|
|
1827
|
+
*/
|
|
1828
|
+
analytics?: AnalyticsDeclaration;
|
|
1829
|
+
/**
|
|
1830
|
+
* That this game keeps state, and roughly how much. See `SaveDeclaration`.
|
|
1831
|
+
*
|
|
1832
|
+
* @category Useful
|
|
1833
|
+
*/
|
|
1834
|
+
save?: SaveDeclaration;
|
|
1835
|
+
/**
|
|
1836
|
+
* The boards this game keeps a score on. See `ScoreDeclaration`.
|
|
1837
|
+
*
|
|
1838
|
+
* @category Useful
|
|
1839
|
+
*/
|
|
1840
|
+
score?: ScoreDeclaration;
|
|
1841
|
+
/**
|
|
1842
|
+
* What a player can be shown to have done. See `AchievementDeclaration`.
|
|
1843
|
+
*
|
|
1844
|
+
* @category Useful
|
|
1845
|
+
*/
|
|
1846
|
+
achievements?: AchievementDeclaration;
|
|
1847
|
+
/**
|
|
1848
|
+
* How this game is played, and what it draws to be played with. See `InputDeclaration`.
|
|
1849
|
+
*
|
|
1850
|
+
* @category Useful
|
|
1851
|
+
*/
|
|
1852
|
+
input?: InputDeclaration;
|
|
1853
|
+
/**
|
|
1854
|
+
* What this game makes noise with, one channel at a time, or `null` for a
|
|
1855
|
+
* game that makes none. See `AudioDeclaration`.
|
|
1856
|
+
*
|
|
1857
|
+
* @category Important
|
|
1858
|
+
*/
|
|
1859
|
+
audio: AudioDeclaration | null;
|
|
1860
|
+
[key: string]: unknown;
|
|
1861
|
+
};
|
|
1862
|
+
/**
|
|
1863
|
+
* A game project's hopple config: `hopple.config.ts`, `hopple.config.js` or
|
|
1864
|
+
* `hopple.config.json`, beside the project's `package.json`.
|
|
1865
|
+
*
|
|
1866
|
+
* Everything the `hopple` command line needs to know about a project, in one
|
|
1867
|
+
* place: how to build the game, which built file is the game, and the manifest
|
|
1868
|
+
* that says what the game is. Only the manifest leaves the machine, uploaded
|
|
1869
|
+
* beside the page as `game.manifest.json`. A TypeScript or JavaScript config
|
|
1870
|
+
* default-exports one of these; a JSON config is one.
|
|
1871
|
+
*
|
|
1872
|
+
* ```ts
|
|
1873
|
+
* import type { HoppleConfig } from '@hopple/sdk';
|
|
1874
|
+
*
|
|
1875
|
+
* export default {
|
|
1876
|
+
* build: 'npm run build',
|
|
1877
|
+
* page: 'dist/index.html',
|
|
1878
|
+
* manifest: { schema: 1, id: 'my-game', version: '0.1.0', title: 'My Game', orientation: 'portrait', sdk: '^0.1.0', audio: null },
|
|
1879
|
+
* } satisfies HoppleConfig;
|
|
1880
|
+
* ```
|
|
1881
|
+
*
|
|
1882
|
+
* @group Manifest
|
|
1883
|
+
* @category Important
|
|
1884
|
+
*/
|
|
1885
|
+
type HoppleConfig = {
|
|
1886
|
+
/** Where an editor finds this file's schema, in a JSON config. Read by nothing else. */
|
|
1887
|
+
$schema?: string;
|
|
1888
|
+
/**
|
|
1889
|
+
* The command that builds the game, run in the config's directory before
|
|
1890
|
+
* `hopple dev`, `check` and `publish` use it, so none of them sees a stale
|
|
1891
|
+
* build. Leave it out for a game with nothing to build.
|
|
1892
|
+
*
|
|
1893
|
+
* @pattern \S
|
|
1894
|
+
* @errorMessage must be the command that builds the game, like "npm run build"
|
|
1895
|
+
*/
|
|
1896
|
+
build?: string;
|
|
1897
|
+
/**
|
|
1898
|
+
* The built page, relative to the config: the one file that is published,
|
|
1899
|
+
* so it has to hold the whole game.
|
|
1900
|
+
*
|
|
1901
|
+
* @pattern \.[Hh][Tt][Mm][Ll]?$
|
|
1902
|
+
* @errorMessage must be the built .html file, like "dist/index.html"
|
|
1903
|
+
*/
|
|
1904
|
+
page: string;
|
|
1905
|
+
/** What the game is. Uploaded beside the page, and read by every host before the game runs. */
|
|
1906
|
+
manifest: GameManifest;
|
|
1907
|
+
};
|
|
1908
|
+
//#endregion
|
|
1909
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/achievements.d.ts
|
|
1910
|
+
/**
|
|
1911
|
+
* Where this player stands on one achievement.
|
|
1912
|
+
*
|
|
1913
|
+
* @group Achievements
|
|
1914
|
+
*/
|
|
1915
|
+
type AchievementStatus<Id extends string = string> = {
|
|
1916
|
+
readonly id: Id;
|
|
1917
|
+
/** Whether it is theirs. */
|
|
1918
|
+
readonly unlocked: boolean;
|
|
1919
|
+
/** The host's clock, when it became theirs. Absent while it is not. */
|
|
1920
|
+
readonly at?: number;
|
|
1921
|
+
/**
|
|
1922
|
+
* The best value reported so far, on a measured achievement. Absent for one
|
|
1923
|
+
* nothing has been reported against, which is not the same as zero.
|
|
1924
|
+
*/
|
|
1925
|
+
readonly progress?: number;
|
|
1926
|
+
};
|
|
1927
|
+
/**
|
|
1928
|
+
* Every achievement this host takes, in the order the manifest declares them,
|
|
1929
|
+
* and whether the host knows where this player stands on them.
|
|
1930
|
+
*
|
|
1931
|
+
* `known: false` is the host not having an answer — a store still replying, a
|
|
1932
|
+
* client that cannot reach its server — and its items carry an id and nothing
|
|
1933
|
+
* else, so there is no `unlocked` to read until `known` has been checked. A
|
|
1934
|
+
* game that drew "locked" there would be telling somebody they had lost
|
|
1935
|
+
* everything because a network was slow.
|
|
1936
|
+
*
|
|
1937
|
+
* @group Achievements
|
|
1938
|
+
*/
|
|
1939
|
+
type AchievementStanding<Id extends string = string> = {
|
|
1940
|
+
readonly known: true;
|
|
1941
|
+
readonly items: readonly AchievementStatus<Id>[];
|
|
1942
|
+
} | {
|
|
1943
|
+
readonly known: false;
|
|
1944
|
+
readonly items: readonly {
|
|
1945
|
+
readonly id: Id;
|
|
1946
|
+
}[];
|
|
1947
|
+
};
|
|
1948
|
+
/** @group Achievements */
|
|
1949
|
+
type AchievementListener<Id extends string = string> = (now: AchievementStanding<Id>, previous: AchievementStanding<Id>) => void;
|
|
1950
|
+
/**
|
|
1951
|
+
* @group Achievements
|
|
1952
|
+
* @category Important
|
|
1953
|
+
*/
|
|
1954
|
+
type Achievements<Id extends string = string, Measured extends string = Id> = {
|
|
1955
|
+
/**
|
|
1956
|
+
* Whether this host keeps achievements at all. Known at the handshake: a
|
|
1957
|
+
* game with none leaves out its achievements screen. Reporting to a host
|
|
1958
|
+
* without them does nothing, so a game need not check before it reports.
|
|
1959
|
+
*/
|
|
1960
|
+
readonly available: boolean;
|
|
1961
|
+
/**
|
|
1962
|
+
* Where this player stands on every achievement, read without asking.
|
|
1963
|
+
*
|
|
1964
|
+
* The same object until something changes, and frozen, so a game can hold
|
|
1965
|
+
* on to it and compare it with `===`.
|
|
1966
|
+
*/
|
|
1967
|
+
readonly standing: AchievementStanding<Id>;
|
|
1968
|
+
/**
|
|
1969
|
+
* Runs when `standing` changes: the host's answer arriving after the welcome,
|
|
1970
|
+
* or a report moving something.
|
|
1971
|
+
*
|
|
1972
|
+
* Not called for the opening standing: that arrived with the welcome, before
|
|
1973
|
+
* there was anything to listen with. Read `standing` once and subscribe.
|
|
1974
|
+
*/
|
|
1975
|
+
on(listener: AchievementListener<Id>): Unsubscribe;
|
|
1976
|
+
/**
|
|
1977
|
+
* This achievement is theirs, whatever its progress says. The host announces
|
|
1978
|
+
* it the first time, so a game that calls this on every level-up shows one
|
|
1979
|
+
* banner.
|
|
1980
|
+
*
|
|
1981
|
+
* Legitimate on a measured achievement too, and it finishes it: the progress
|
|
1982
|
+
* goes to the target rather than leaving something unlocked at 40 of 100.
|
|
1983
|
+
*
|
|
1984
|
+
* An id this game's manifest never declared is reported to `onError`, as an
|
|
1985
|
+
* author's mistake and not a player's.
|
|
1986
|
+
*/
|
|
1987
|
+
unlock(id: Id): void;
|
|
1988
|
+
/**
|
|
1989
|
+
* How far along a measured achievement this player is, as an absolute value.
|
|
1990
|
+
*
|
|
1991
|
+
* *At 40*, not *add one*. The host keeps whichever value the manifest calls
|
|
1992
|
+
* better — the larger on an ordinary count, the smaller on a best time — so
|
|
1993
|
+
* reporting the same number twice does nothing, and reporting a worse one is
|
|
1994
|
+
* not a regression. Reaching the target unlocks it.
|
|
1995
|
+
*
|
|
1996
|
+
* Against an achievement declared as a flag, the host refuses it and the
|
|
1997
|
+
* refusal is reported to `onError`: there is no target to move toward, and a
|
|
1998
|
+
* host that quietly unlocked it would be hiding the mistake.
|
|
1999
|
+
*/
|
|
2000
|
+
progress(id: Measured, value: number): void;
|
|
2001
|
+
};
|
|
2002
|
+
//#endregion
|
|
2003
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/analytics.d.ts
|
|
2004
|
+
/**
|
|
2005
|
+
* @group Analytics
|
|
2006
|
+
* @category Important
|
|
2007
|
+
*/
|
|
2008
|
+
type Analytics<Events extends object = Record<string, AnalyticsProps>> = {
|
|
2009
|
+
/**
|
|
2010
|
+
* Say that something happened.
|
|
2011
|
+
*
|
|
2012
|
+
* The name must be one the game declared in its manifest, or the host will
|
|
2013
|
+
* refuse it. A name or a prop that cannot travel goes to `onError` — the game
|
|
2014
|
+
* author's own channel, so a mistake is loud where it is cheap and harmless
|
|
2015
|
+
* where it is not — and a refused prop costs its column rather than the event.
|
|
2016
|
+
*/
|
|
2017
|
+
track<Event extends keyof Events & string>(name: Event, props?: Events[Event]): void;
|
|
2018
|
+
};
|
|
2019
|
+
//#endregion
|
|
2020
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/audio.d.ts
|
|
2021
|
+
/**
|
|
2022
|
+
* Something about the game's sound changed: the host's order, the player's
|
|
2023
|
+
* mute, a fader. Read what you need — `volume(channel)`, `allowed` — and apply
|
|
2024
|
+
* it at once.
|
|
2025
|
+
*
|
|
2026
|
+
* @group Audio
|
|
2027
|
+
*/
|
|
2028
|
+
type AudioListener = () => void;
|
|
2029
|
+
/**
|
|
2030
|
+
* @group Audio
|
|
2031
|
+
* @category Important
|
|
2032
|
+
*/
|
|
2033
|
+
type Audio<Channel extends string = string> = {
|
|
2034
|
+
/**
|
|
2035
|
+
* Whether the host lets the game make sound now. Known as soon as `init`
|
|
2036
|
+
* resolves.
|
|
2037
|
+
*
|
|
2038
|
+
* `volume()` is already 0 while this is `false`, so a game that mixes from
|
|
2039
|
+
* `volume()` obeys it without reading it. Read it to pause an engine's own
|
|
2040
|
+
* sound rather than leave it playing silently.
|
|
2041
|
+
*/
|
|
2042
|
+
readonly allowed: boolean;
|
|
2043
|
+
/**
|
|
2044
|
+
* Everything off, whatever the faders say.
|
|
2045
|
+
*
|
|
2046
|
+
* Not the same fact as `master === 0`, and a game must not fold the two
|
|
2047
|
+
* together: a player who mutes and unmutes expects the mix they had back.
|
|
2048
|
+
*/
|
|
2049
|
+
readonly muted: boolean;
|
|
2050
|
+
/** The fader over the whole game, `0`–`1`, ignoring mute. */
|
|
2051
|
+
readonly master: number;
|
|
2052
|
+
/** The whole of what the host last said. Frozen; a new object on every change. */
|
|
2053
|
+
readonly state: AudioState;
|
|
2054
|
+
/**
|
|
2055
|
+
* How loud one of this game's channels should actually come out, `0`–`1`.
|
|
2056
|
+
*
|
|
2057
|
+
* **The number to multiply a sound by, and the only one worth reading.** It
|
|
2058
|
+
* is the host's order, mute, the master fader and the channel's own, already
|
|
2059
|
+
* folded — so a game
|
|
2060
|
+
* that writes `gain.value = sdk.audio.volume('music')` is correct, and one
|
|
2061
|
+
* that reads the three separately is one refactor away from forgetting mute.
|
|
2062
|
+
*
|
|
2063
|
+
* A channel this game never declared reads as its master, which is what a
|
|
2064
|
+
* game asking about a channel its manifest does not carry deserves: audible,
|
|
2065
|
+
* and governed by the one fader that certainly exists. Called with nothing,
|
|
2066
|
+
* or with `master`, it is the master fader itself.
|
|
2067
|
+
*/
|
|
2068
|
+
volume(channel?: Channel): number;
|
|
2069
|
+
/**
|
|
2070
|
+
* The channel's own fader, unmixed and ignoring mute.
|
|
2071
|
+
*
|
|
2072
|
+
* For a game drawing its own volume screen. What a sound is multiplied by is
|
|
2073
|
+
* `volume()`, and mixing with this one instead is how a mute button stops
|
|
2074
|
+
* working.
|
|
2075
|
+
*/
|
|
2076
|
+
channel(id: Channel): number;
|
|
2077
|
+
/**
|
|
2078
|
+
* Told whenever any of it changes: the host's order, and the moment the
|
|
2079
|
+
* player mutes or moves a fader.
|
|
2080
|
+
*
|
|
2081
|
+
* **Apply it at once.** The host is answered as soon as every listener has
|
|
2082
|
+
* been called, and nothing returned is waited for — a mute that fades out
|
|
2083
|
+
* over half a second is half a second of sound the player asked not to hear.
|
|
2084
|
+
* A ramp of a few milliseconds, to keep a gain jump from clicking, is fine;
|
|
2085
|
+
* it just is not something to wait on.
|
|
2086
|
+
*
|
|
2087
|
+
* Not called for the opening state: that arrived with the welcome, before
|
|
2088
|
+
* there was anything to listen with. Apply it once and subscribe.
|
|
2089
|
+
*/
|
|
2090
|
+
on(listener: AudioListener): Unsubscribe;
|
|
2091
|
+
/**
|
|
2092
|
+
* A gain node for one channel of a Web Audio context, kept at that channel's
|
|
2093
|
+
* volume: connect every sound on the channel to it instead of to
|
|
2094
|
+
* `context.destination`.
|
|
2095
|
+
*
|
|
2096
|
+
* The context is looked after too: suspended whenever sound is not
|
|
2097
|
+
* `allowed` (and the host told only once it has gone quiet), resumed when it
|
|
2098
|
+
* is, and resumed by the player's first press in the frame where the browser
|
|
2099
|
+
* started it suspended. The same context and channel always give the same
|
|
2100
|
+
* node.
|
|
2101
|
+
*/
|
|
2102
|
+
output(context: AudioContext, channel?: Channel): GainNode;
|
|
2103
|
+
/**
|
|
2104
|
+
* Keep an `<audio>` or `<video>` element at one channel's volume, and paused
|
|
2105
|
+
* whenever sound is not `allowed` — playing again when it is, if it was
|
|
2106
|
+
* playing when it stopped.
|
|
2107
|
+
*
|
|
2108
|
+
* Returns a function that lets the element go again.
|
|
2109
|
+
*/
|
|
2110
|
+
media(element: HTMLMediaElement, channel?: Channel): Unsubscribe;
|
|
2111
|
+
};
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/info.d.ts
|
|
2114
|
+
/**
|
|
2115
|
+
* What the host said, frozen.
|
|
2116
|
+
*
|
|
2117
|
+
* Frozen because it is a record of what a game was told, and a game that could
|
|
2118
|
+
* edit it would be writing a bug that only surfaces somewhere else entirely,
|
|
2119
|
+
* when something reads the field back and believes it.
|
|
2120
|
+
*
|
|
2121
|
+
* @group Info
|
|
2122
|
+
* @category Important
|
|
2123
|
+
*/
|
|
2124
|
+
type Info = Readonly<{
|
|
2125
|
+
app: Readonly<AppInfo>;
|
|
2126
|
+
game: Readonly<GameInfo>;
|
|
2127
|
+
player: Readonly<PlayerInfo>;
|
|
2128
|
+
device: Readonly<DeviceInfo>;
|
|
2129
|
+
}>;
|
|
2130
|
+
//#endregion
|
|
2131
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/layout.d.ts
|
|
2132
|
+
/**
|
|
2133
|
+
* How much of each edge belongs to the host. CSS pixels.
|
|
2134
|
+
*
|
|
2135
|
+
* @group Layout
|
|
2136
|
+
* @category Useful
|
|
2137
|
+
*/
|
|
2138
|
+
type Insets = {
|
|
2139
|
+
top: number;
|
|
2140
|
+
right: number;
|
|
2141
|
+
bottom: number;
|
|
2142
|
+
left: number;
|
|
2143
|
+
};
|
|
2144
|
+
/**
|
|
2145
|
+
* What is over the game. Facts only — no projection, because which projection
|
|
2146
|
+
* is the right one is the game's to decide.
|
|
2147
|
+
*
|
|
2148
|
+
* @group Layout
|
|
2149
|
+
*/
|
|
2150
|
+
type LayoutState = {
|
|
2151
|
+
/**
|
|
2152
|
+
* The frame the rectangles are measured against, as the host last set it:
|
|
2153
|
+
* the page's own window, so `innerWidth` and `innerHeight` agree with it from
|
|
2154
|
+
* the moment `init` resolves.
|
|
2155
|
+
*/
|
|
2156
|
+
readonly frame: Size;
|
|
2157
|
+
/**
|
|
2158
|
+
* Everything over the frame, whoever put it there: the host's rail and the
|
|
2159
|
+
* device's notch, and the SDK's own on-screen controls where a game is
|
|
2160
|
+
* showing some. One list, because a game avoids all of them the same way.
|
|
2161
|
+
*/
|
|
2162
|
+
readonly obstructions: readonly Obstruction[];
|
|
2163
|
+
/**
|
|
2164
|
+
* The default projection of everything reserved onto the four edges, for the
|
|
2165
|
+
* majority of games that want exactly that.
|
|
2166
|
+
*
|
|
2167
|
+
* It is a convenience, not the truth: a game that wants to ignore cutouts, or
|
|
2168
|
+
* to keep clear of overlays as well, calls `insetsFrom` over whatever subset
|
|
2169
|
+
* of `obstructions` it means. A game that scrolls wants a second reading
|
|
2170
|
+
* beside this one — see `InsetPolicy`.
|
|
2171
|
+
*/
|
|
2172
|
+
readonly insets: Insets;
|
|
2173
|
+
};
|
|
2174
|
+
/** @group Layout */
|
|
2175
|
+
type LayoutListener = (now: LayoutState, previous: LayoutState) => void;
|
|
2176
|
+
/**
|
|
2177
|
+
* @group Layout
|
|
2178
|
+
* @category Important
|
|
2179
|
+
*/
|
|
2180
|
+
type Layout = LayoutState & {
|
|
2181
|
+
/** Runs whenever the frame or the rectangles change. */
|
|
2182
|
+
on(listener: LayoutListener): Unsubscribe;
|
|
2183
|
+
};
|
|
2184
|
+
/**
|
|
2185
|
+
* Which rectangles an inset is being asked about.
|
|
2186
|
+
*
|
|
2187
|
+
* `all` is the ordinary reading and the default. `strips` is for the one thing
|
|
2188
|
+
* insets cannot express: see `only` below.
|
|
2189
|
+
*
|
|
2190
|
+
* @group Layout
|
|
2191
|
+
* @category Useful
|
|
2192
|
+
*/
|
|
2193
|
+
type InsetPolicy = {
|
|
2194
|
+
/**
|
|
2195
|
+
* `all` — every reserved rectangle. `strips` — only those that run the whole
|
|
2196
|
+
* length of an edge, which on a real host means the rail and nothing else.
|
|
2197
|
+
*
|
|
2198
|
+
* The distinction exists because of scrollbars, and it is worth spelling out
|
|
2199
|
+
* since nothing else in this file has to care. A scrollbar is painted on the
|
|
2200
|
+
* border edge of whatever scrolls, and padding lives inside that edge — so a
|
|
2201
|
+
* game that scrolls cannot move its scrollbar off the host's rail by paying
|
|
2202
|
+
* `insets`, however honestly it pays them. The only thing that moves it is
|
|
2203
|
+
* making the scrolling element itself stop short, which means insets that say
|
|
2204
|
+
* *the host has taken this whole edge* rather than *the host is somewhere over
|
|
2205
|
+
* here*.
|
|
2206
|
+
*
|
|
2207
|
+
* A corner control is the case the two answers differ on, and `strips` drops
|
|
2208
|
+
* it deliberately: it costs the least area on a side edge, so `all` turns a
|
|
2209
|
+
* 56-pixel square into a 56-pixel column down the entire height — most of a
|
|
2210
|
+
* sidebar's worth of scrolling content for one button. What a game loses by
|
|
2211
|
+
* dropping it is the far end of its scrollbar, under a corner the host's
|
|
2212
|
+
* control was always going to sit on.
|
|
2213
|
+
*
|
|
2214
|
+
* Content is the other half and is not this function's problem: keeping text
|
|
2215
|
+
* and buttons off a corner control is what `all` and a padding are for, and
|
|
2216
|
+
* a game doing both reads this twice. The demo does.
|
|
2217
|
+
*/
|
|
2218
|
+
only?: 'all' | 'strips';
|
|
2219
|
+
};
|
|
2220
|
+
/**
|
|
2221
|
+
* The cheapest set of edge insets that covers every reserved rectangle.
|
|
2222
|
+
*
|
|
2223
|
+
* Each rectangle is pushed onto one of the four edges — the one where it costs
|
|
2224
|
+
* the least area, which for a corner is the shorter way out. That is what makes
|
|
2225
|
+
* the same corner control a top inset on a phone and a right inset on a desktop
|
|
2226
|
+
* window without anybody deciding it twice.
|
|
2227
|
+
*
|
|
2228
|
+
* A rectangle touching no edge at all still has to be covered by something,
|
|
2229
|
+
* since an inset is all this can say; it gets pushed to its nearest edge and
|
|
2230
|
+
* takes the gap with it. Hosts are expected not to do that, and a game that
|
|
2231
|
+
* wants the truth about one should read the rectangles.
|
|
2232
|
+
*
|
|
2233
|
+
* Exported because it is the whole policy in one pure function: a game that
|
|
2234
|
+
* wants to project differently, or against a size of its own choosing, can.
|
|
2235
|
+
*
|
|
2236
|
+
* @group Layout
|
|
2237
|
+
* @category Useful
|
|
2238
|
+
*/
|
|
2239
|
+
declare function insetsFrom(obstructions: readonly Obstruction[], frame: Size, policy?: InsetPolicy): Insets;
|
|
2240
|
+
//#endregion
|
|
2241
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/names.d.ts
|
|
2242
|
+
/**
|
|
2243
|
+
* The keys of a declared map, or `string` where the map is not a literal one —
|
|
2244
|
+
* which is what a manifest typed as the wide `GameManifest` gives — and
|
|
2245
|
+
* `never` where nothing is declared.
|
|
2246
|
+
*
|
|
2247
|
+
* @group Names
|
|
2248
|
+
*/
|
|
2249
|
+
type DeclaredNames<T> = T extends object ? string extends keyof T ? string : Extract<keyof T, string> : never;
|
|
2250
|
+
/**
|
|
2251
|
+
* An action the manifest declares under `input.actions`.
|
|
2252
|
+
*
|
|
2253
|
+
* @group Names
|
|
2254
|
+
*/
|
|
2255
|
+
type ActionName<M> = M extends {
|
|
2256
|
+
input?: {
|
|
2257
|
+
actions?: infer A;
|
|
2258
|
+
};
|
|
2259
|
+
} ? DeclaredNames<NonNullable<A>> : never;
|
|
2260
|
+
/**
|
|
2261
|
+
* An on-screen control layout the manifest declares under `input.controls`.
|
|
2262
|
+
*
|
|
2263
|
+
* @group Names
|
|
2264
|
+
*/
|
|
2265
|
+
type LayoutName<M> = M extends {
|
|
2266
|
+
input?: {
|
|
2267
|
+
controls?: infer C;
|
|
2268
|
+
};
|
|
2269
|
+
} ? DeclaredNames<NonNullable<C>> : never;
|
|
2270
|
+
/**
|
|
2271
|
+
* A score board the manifest declares under `score.boards`.
|
|
2272
|
+
*
|
|
2273
|
+
* @group Names
|
|
2274
|
+
*/
|
|
2275
|
+
type BoardName<M> = M extends {
|
|
2276
|
+
score?: {
|
|
2277
|
+
boards?: infer B;
|
|
2278
|
+
};
|
|
2279
|
+
} ? DeclaredNames<NonNullable<B>> : never;
|
|
2280
|
+
/**
|
|
2281
|
+
* An achievement the manifest declares under `achievements.list`.
|
|
2282
|
+
*
|
|
2283
|
+
* @group Names
|
|
2284
|
+
*/
|
|
2285
|
+
type AchievementName<M> = M extends {
|
|
2286
|
+
achievements?: {
|
|
2287
|
+
list?: infer L;
|
|
2288
|
+
};
|
|
2289
|
+
} ? DeclaredNames<NonNullable<L>> : never;
|
|
2290
|
+
/**
|
|
2291
|
+
* An achievement declared with `progress`: the only kind `progress()` takes.
|
|
2292
|
+
*
|
|
2293
|
+
* @group Names
|
|
2294
|
+
*/
|
|
2295
|
+
type MeasuredName<M> = M extends {
|
|
2296
|
+
achievements?: {
|
|
2297
|
+
list?: infer L;
|
|
2298
|
+
};
|
|
2299
|
+
} ? string extends keyof NonNullable<L> ? string : { [K in keyof NonNullable<L>]: NonNullable<L>[K] extends {
|
|
2300
|
+
progress: object;
|
|
2301
|
+
} ? K : never; }[keyof NonNullable<L>] & string : never;
|
|
2302
|
+
/**
|
|
2303
|
+
* An audio channel the manifest declares under `audio.channels`.
|
|
2304
|
+
*
|
|
2305
|
+
* @group Names
|
|
2306
|
+
*/
|
|
2307
|
+
type ChannelName<M> = M extends {
|
|
2308
|
+
audio?: {
|
|
2309
|
+
channels?: infer C;
|
|
2310
|
+
} | null;
|
|
2311
|
+
} ? DeclaredNames<NonNullable<C>> : never;
|
|
2312
|
+
/**
|
|
2313
|
+
* An analytics event the manifest declares under `analytics.events`.
|
|
2314
|
+
*
|
|
2315
|
+
* @group Names
|
|
2316
|
+
*/
|
|
2317
|
+
type EventName<M> = M extends {
|
|
2318
|
+
analytics?: {
|
|
2319
|
+
events?: infer E;
|
|
2320
|
+
};
|
|
2321
|
+
} ? DeclaredNames<NonNullable<E>> : never;
|
|
2322
|
+
/**
|
|
2323
|
+
* What a property declared as `string`, `number` or `boolean` holds.
|
|
2324
|
+
*
|
|
2325
|
+
* @group Names
|
|
2326
|
+
*/
|
|
2327
|
+
type DeclaredValue<T> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : AnalyticsValue;
|
|
2328
|
+
/**
|
|
2329
|
+
* Every declared event with the properties it was declared with, each
|
|
2330
|
+
* optional and typed as declared.
|
|
2331
|
+
*
|
|
2332
|
+
* @group Names
|
|
2333
|
+
*/
|
|
2334
|
+
type EventProps<M> = M extends {
|
|
2335
|
+
analytics?: {
|
|
2336
|
+
events?: infer E;
|
|
2337
|
+
};
|
|
2338
|
+
} ? string extends keyof NonNullable<E> ? Record<string, AnalyticsProps> : { [K in keyof NonNullable<E> & string]: { [P in keyof NonNullable<E>[K]]?: DeclaredValue<NonNullable<E>[K][P]>; }; } : Record<never, never>;
|
|
2339
|
+
/**
|
|
2340
|
+
* One of the sticks a game reads: a gamepad's, or an on-screen thumbstick
|
|
2341
|
+
* acting as one, or an on-screen thumbstick of its own (`controls.<name>`).
|
|
2342
|
+
* `pad.stick` is whichever of a pad's two the player is using.
|
|
2343
|
+
*
|
|
2344
|
+
* @group Names
|
|
2345
|
+
*/
|
|
2346
|
+
type StickName = 'pad.left.stick' | 'pad.right.stick' | 'pad.stick' | `controls.${string}`;
|
|
2347
|
+
//#endregion
|
|
2348
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/input/state.d.ts
|
|
2349
|
+
/**
|
|
2350
|
+
* Where the pointer is, in the frame's own coordinates.
|
|
2351
|
+
*
|
|
2352
|
+
* @group Input
|
|
2353
|
+
*/
|
|
2354
|
+
type PointerState = {
|
|
2355
|
+
x: number;
|
|
2356
|
+
y: number;
|
|
2357
|
+
/** Whether its primary button — or the finger — is down. */
|
|
2358
|
+
down: boolean;
|
|
2359
|
+
};
|
|
2360
|
+
/**
|
|
2361
|
+
* How far a stick is pushed, each axis in −1..1.
|
|
2362
|
+
*
|
|
2363
|
+
* @group Input
|
|
2364
|
+
*/
|
|
2365
|
+
type StickState = {
|
|
2366
|
+
x: number;
|
|
2367
|
+
y: number;
|
|
2368
|
+
};
|
|
2369
|
+
/**
|
|
2370
|
+
* @group Input
|
|
2371
|
+
*/
|
|
2372
|
+
type ActionListener<Action extends string = string> = (down: boolean, action: Action) => void;
|
|
2373
|
+
/**
|
|
2374
|
+
* @group Input
|
|
2375
|
+
*/
|
|
2376
|
+
type StickListener = (value: StickState, stick: string) => void;
|
|
2377
|
+
/**
|
|
2378
|
+
* @group Input
|
|
2379
|
+
*/
|
|
2380
|
+
type PointerListener = (pointer: PointerState | null) => void;
|
|
2381
|
+
/**
|
|
2382
|
+
* What was true at one moment, and what has happened since the last one.
|
|
2383
|
+
*
|
|
2384
|
+
* Frozen and detached: reading a snapshot twice in a frame gives the same
|
|
2385
|
+
* answers, and holding one past the frame it was taken in tells the truth about
|
|
2386
|
+
* a moment that has passed rather than lying about the present.
|
|
2387
|
+
*
|
|
2388
|
+
* @group Input
|
|
2389
|
+
*/
|
|
2390
|
+
type InputSnapshot<Action extends string = string> = {
|
|
2391
|
+
/** Whether the action is down right now. */
|
|
2392
|
+
pressed(action: Action): boolean;
|
|
2393
|
+
/**
|
|
2394
|
+
* Whether it went down since the last read.
|
|
2395
|
+
*
|
|
2396
|
+
* True even when it has already come back up — a tap shorter than one frame
|
|
2397
|
+
* is a real press that pure level polling would miss entirely, and the
|
|
2398
|
+
* commonest thing a player does on a touchscreen.
|
|
2399
|
+
*/
|
|
2400
|
+
started(action: Action): boolean;
|
|
2401
|
+
/** Whether it came up since the last read. */
|
|
2402
|
+
ended(action: Action): boolean;
|
|
2403
|
+
/** How far a stick is pushed. `pad.stick` reads whichever one is off centre. */
|
|
2404
|
+
stick(stick: StickName): StickState;
|
|
2405
|
+
/** Where the pointer is, or `null` for a pointer that is nowhere. */
|
|
2406
|
+
pointer: PointerState | null;
|
|
2407
|
+
};
|
|
2408
|
+
//#endregion
|
|
2409
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/input/controls.d.ts
|
|
2410
|
+
/**
|
|
2411
|
+
* The name a host leaves its input shim under.
|
|
2412
|
+
*
|
|
2413
|
+
* Beside `SHIM` in spirit and separate from it in fact: that one is a channel a
|
|
2414
|
+
* platform must provide for a game to speak at all, and this one is an opinion
|
|
2415
|
+
* about drawing that most hosts will not have. A game with no shim draws the
|
|
2416
|
+
* default controls and is completely playable — which is what an embed on
|
|
2417
|
+
* somebody else's page gets, since there is nowhere for anyone to inject one.
|
|
2418
|
+
*
|
|
2419
|
+
* @group Input
|
|
2420
|
+
*/
|
|
2421
|
+
declare const INPUT_SHIM = "__HOPPLE_INPUT__";
|
|
2422
|
+
/**
|
|
2423
|
+
* One control, as a renderer is given it. Resolved: no ids left to look up.
|
|
2424
|
+
*
|
|
2425
|
+
* @group Input
|
|
2426
|
+
*/
|
|
2427
|
+
type ControlModelSlot = {
|
|
2428
|
+
id: string;
|
|
2429
|
+
type: 'buttons' | 'thumbstick';
|
|
2430
|
+
/** Always a shape this SDK knows. An unfamiliar one has already become `column`. */
|
|
2431
|
+
shape: InputLayoutShape;
|
|
2432
|
+
/**
|
|
2433
|
+
* For `buttons`: what each one presses, what to draw on it, and the standard
|
|
2434
|
+
* control it stands in for where the layout said one.
|
|
2435
|
+
*/
|
|
2436
|
+
buttons: readonly {
|
|
2437
|
+
action: string;
|
|
2438
|
+
label: string;
|
|
2439
|
+
as?: string;
|
|
2440
|
+
}[];
|
|
2441
|
+
/** For `thumbstick`: the stick id it drives. */
|
|
2442
|
+
stick: string;
|
|
2443
|
+
/**
|
|
2444
|
+
* For `thumbstick`: the one axis it moves along, where the layout held it to
|
|
2445
|
+
* one. Absent, it moves both ways. A renderer that draws it freely anyway is
|
|
2446
|
+
* still correct: the SDK zeroes the other axis of whatever is reported.
|
|
2447
|
+
*/
|
|
2448
|
+
axis?: InputLayoutAxis;
|
|
2449
|
+
};
|
|
2450
|
+
/**
|
|
2451
|
+
* @group Input
|
|
2452
|
+
*/
|
|
2453
|
+
type ControlModelZone = {
|
|
2454
|
+
zone: InputLayoutZone;
|
|
2455
|
+
controls: readonly ControlModelSlot[];
|
|
2456
|
+
};
|
|
2457
|
+
/**
|
|
2458
|
+
* A whole layout, resolved against the manifest's actions.
|
|
2459
|
+
*
|
|
2460
|
+
* @group Input
|
|
2461
|
+
*/
|
|
2462
|
+
type ControlModel = {
|
|
2463
|
+
/** Which named layout this is. */
|
|
2464
|
+
layout: string;
|
|
2465
|
+
zones: readonly ControlModelZone[];
|
|
2466
|
+
/** The frame the controls are being drawn on, when it is known. */
|
|
2467
|
+
frame: Size;
|
|
2468
|
+
/**
|
|
2469
|
+
* How much of that frame the host has taken along each edge.
|
|
2470
|
+
*
|
|
2471
|
+
* The host's furniture only — its rail, its corner control, the device's
|
|
2472
|
+
* notch. Emphatically *not* the pad's own rectangles, which reach a game as
|
|
2473
|
+
* obstructions of kind `controls` and would, if they were projected in here,
|
|
2474
|
+
* inset the pad by the pad on every pass until it walked off the frame.
|
|
2475
|
+
* Whoever fills this in owes that filter; `index.ts` pays it.
|
|
2476
|
+
*
|
|
2477
|
+
* Zero is the ordinary answer and always a safe one: a host that reserved the
|
|
2478
|
+
* frame instead of drawing over it has already shrunk `frame` clear of its
|
|
2479
|
+
* own furniture, and a host too old to say anything has said nothing. Both
|
|
2480
|
+
* land here as four zeroes and the pad sits where it always sat.
|
|
2481
|
+
*/
|
|
2482
|
+
insets: Insets;
|
|
2483
|
+
/**
|
|
2484
|
+
* How a thumbstick meets a thumb, as the player set it on their host.
|
|
2485
|
+
*
|
|
2486
|
+
* `floating`: drawn where the layout puts it, so a player can see there is a
|
|
2487
|
+
* stick, and brought to wherever a thumb lands on its side of the screen, so
|
|
2488
|
+
* the place they first touch is the centre. `fixed`: moved only from where
|
|
2489
|
+
* it is drawn. A renderer that ignores this draws `fixed`, which is still a
|
|
2490
|
+
* stick a game can be played with.
|
|
2491
|
+
*/
|
|
2492
|
+
sticks: InputSticks;
|
|
2493
|
+
};
|
|
2494
|
+
/**
|
|
2495
|
+
* How a renderer says something happened.
|
|
2496
|
+
*
|
|
2497
|
+
* The only way out of a renderer, replaced or not. A shim reports and the SDK
|
|
2498
|
+
* decides: that is what keeps a replaced renderer from being able to leave an
|
|
2499
|
+
* action held forever.
|
|
2500
|
+
*
|
|
2501
|
+
* @group Input
|
|
2502
|
+
*/
|
|
2503
|
+
type ControlReport = {
|
|
2504
|
+
/** `index` is into the control's own `buttons`. */
|
|
2505
|
+
press(control: string, index: number, down: boolean): void;
|
|
2506
|
+
/** A stick's position, each axis in −1..1 and already inside the unit circle. */
|
|
2507
|
+
move(control: string, x: number, y: number): void;
|
|
2508
|
+
};
|
|
2509
|
+
/**
|
|
2510
|
+
* What a host may install to change how controls are drawn.
|
|
2511
|
+
*
|
|
2512
|
+
* **Every hook is optional and every one is wrapped.** A shim that throws is
|
|
2513
|
+
* dropped back to the default for that hook and reported to `onError`, because
|
|
2514
|
+
* a replaceable renderer is otherwise a way for one host to ship a broken pad to
|
|
2515
|
+
* every game it runs at once.
|
|
2516
|
+
*
|
|
2517
|
+
* **The SDK declares the version and the shim adapts.** This file is frozen
|
|
2518
|
+
* inside bundles that shipped months before any given shim was written, so the
|
|
2519
|
+
* dangerous direction is a new shim meeting an old SDK — and it is safe by
|
|
2520
|
+
* construction if the shim is the side doing the adapting. A shim reads
|
|
2521
|
+
* `request.contract` and answers with what that version can carry.
|
|
2522
|
+
*
|
|
2523
|
+
* @group Input
|
|
2524
|
+
*/
|
|
2525
|
+
type InputShim = {
|
|
2526
|
+
/** The contract version this shim was written against. `1` today. */
|
|
2527
|
+
v: 1;
|
|
2528
|
+
/**
|
|
2529
|
+
* Whether to draw controls for this layout at all.
|
|
2530
|
+
*
|
|
2531
|
+
* The host's half of the answer; the game's half is having asked. Returning
|
|
2532
|
+
* nothing leaves the decision alone, which is what a shim that only wanted to
|
|
2533
|
+
* restyle should do.
|
|
2534
|
+
*/
|
|
2535
|
+
decide?(request: {
|
|
2536
|
+
layout: string;
|
|
2537
|
+
frame: Size;
|
|
2538
|
+
/** What the host has taken along each edge — see `ControlModel.insets`. */
|
|
2539
|
+
insets: Insets;
|
|
2540
|
+
contract: number;
|
|
2541
|
+
}): boolean | undefined;
|
|
2542
|
+
/**
|
|
2543
|
+
* How far a stick must move before it counts, 0 to 1.
|
|
2544
|
+
*
|
|
2545
|
+
* The host's rather than the game's, because it is an accessibility setting
|
|
2546
|
+
* before it is a taste: a player whose hands shake needs a wider dead region
|
|
2547
|
+
* in every game they open, not in the ones that happened to think of it.
|
|
2548
|
+
*/
|
|
2549
|
+
deadzone?: number;
|
|
2550
|
+
/** A tick when a control is pressed. Absent where the platform has none. */
|
|
2551
|
+
haptic?(kind: 'press' | 'release'): void;
|
|
2552
|
+
/** Called with the root once, to set custom properties or add a stylesheet. */
|
|
2553
|
+
style?(root: ShadowRoot, request: {
|
|
2554
|
+
layout: string;
|
|
2555
|
+
frame: Size;
|
|
2556
|
+
}): void;
|
|
2557
|
+
/**
|
|
2558
|
+
* Draw the controls instead of the SDK.
|
|
2559
|
+
*
|
|
2560
|
+
* Given the root, the resolved model and the one way back. Returns a teardown,
|
|
2561
|
+
* which is called before the next render and when the controls are hidden.
|
|
2562
|
+
*/
|
|
2563
|
+
render?(root: ShadowRoot, model: ControlModel, report: ControlReport): (() => void) | void;
|
|
2564
|
+
};
|
|
2565
|
+
/**
|
|
2566
|
+
* @group Input
|
|
2567
|
+
*/
|
|
2568
|
+
declare function isInputShim(value: unknown): value is InputShim;
|
|
2569
|
+
/**
|
|
2570
|
+
* The contract version this SDK speaks. Told to a shim so it can answer in kind.
|
|
2571
|
+
*
|
|
2572
|
+
* `2` added `insets` — to the model a renderer is given, to what `decide` is
|
|
2573
|
+
* asked, and as four custom properties the default sheet spends. Purely
|
|
2574
|
+
* additive: a `v: 1` shim that reads none of them behaves exactly as it did.
|
|
2575
|
+
*
|
|
2576
|
+
* `3` added `axis` to a thumbstick in the model. Additive again: a shim that
|
|
2577
|
+
* draws the stick moving both ways still gives the game one axis, because the
|
|
2578
|
+
* other is dropped from what it reports.
|
|
2579
|
+
*
|
|
2580
|
+
* `4` added `sticks` to the model: whether the player wants a thumbstick that
|
|
2581
|
+
* comes to their thumb. Additive: a shim that reads nothing of it draws the
|
|
2582
|
+
* stick where it always did.
|
|
2583
|
+
*
|
|
2584
|
+
* @group Input
|
|
2585
|
+
*/
|
|
2586
|
+
declare const INPUT_CONTRACT = 4;
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/input/bindings.d.ts
|
|
2589
|
+
/**
|
|
2590
|
+
* The standard mapping's buttons, by index, as the names this SDK uses.
|
|
2591
|
+
*
|
|
2592
|
+
* `Gamepad.mapping === 'standard'` is what makes these indices mean anything;
|
|
2593
|
+
* a pad that reports anything else is read for its axes and ignored for its
|
|
2594
|
+
* buttons, because an unmapped index is a number with no meaning attached.
|
|
2595
|
+
*
|
|
2596
|
+
* **The left cluster is the d-pad and the right is the faces**, which is what
|
|
2597
|
+
* makes `pad.south` cover both *A* and *down* with one word. Compass positions
|
|
2598
|
+
* rather than letters throughout: A and B are swapped between the two
|
|
2599
|
+
* best-selling controllers, so a name built on them is wrong for half the
|
|
2600
|
+
* players who read it.
|
|
2601
|
+
*
|
|
2602
|
+
* @group Input
|
|
2603
|
+
*/
|
|
2604
|
+
declare const PAD_BUTTONS: readonly (string | null)[];
|
|
2605
|
+
/**
|
|
2606
|
+
* The binding a player would actually use, given what they are holding.
|
|
2607
|
+
*
|
|
2608
|
+
* A game that draws one prompt for an action with four bindings has to be told
|
|
2609
|
+
* which one, and the answer is the one their hands are on. Falls back to the
|
|
2610
|
+
* first declared binding, which is the game author's own idea of the primary
|
|
2611
|
+
* one.
|
|
2612
|
+
*
|
|
2613
|
+
* @group Input
|
|
2614
|
+
* @category Useful
|
|
2615
|
+
*/
|
|
2616
|
+
declare function preferredBinding(bind: readonly string[], scheme: InputScheme): string | undefined;
|
|
2617
|
+
/**
|
|
2618
|
+
* Whether a binding is one the player could press with what they are using.
|
|
2619
|
+
*
|
|
2620
|
+
* @group Input
|
|
2621
|
+
* @category Useful
|
|
2622
|
+
*/
|
|
2623
|
+
declare function servesScheme(binding: string, scheme: InputScheme): boolean;
|
|
2624
|
+
/**
|
|
2625
|
+
* What to write on the screen for one binding.
|
|
2626
|
+
*
|
|
2627
|
+
* **Best effort, and the host is allowed to do better.** Whether the south face
|
|
2628
|
+
* button says *A* or *B* depends on whose controller it is, and
|
|
2629
|
+
* `Gamepad.id` is free text that varies by browser and by operating system — so
|
|
2630
|
+
* a host that genuinely knows, because it is a native app, sends its own label
|
|
2631
|
+
* and this is what fills in when nobody did. Getting it slightly wrong is a
|
|
2632
|
+
* prompt a player can still follow; refusing to draw one is a game that never
|
|
2633
|
+
* says which button to press.
|
|
2634
|
+
*
|
|
2635
|
+
* @group Input
|
|
2636
|
+
* @category Useful
|
|
2637
|
+
*/
|
|
2638
|
+
declare function labelFor(binding: string, scheme: InputScheme): string;
|
|
2639
|
+
//#endregion
|
|
2640
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/input/index.d.ts
|
|
2641
|
+
/**
|
|
2642
|
+
* How far a stick moves before it counts, where no shim has an opinion.
|
|
2643
|
+
*
|
|
2644
|
+
* @group Input
|
|
2645
|
+
* @category Useful
|
|
2646
|
+
*/
|
|
2647
|
+
declare const DEADZONE = 0.15;
|
|
2648
|
+
/**
|
|
2649
|
+
* What presses each action, and what the player is pressing with: the part of
|
|
2650
|
+
* input that is state rather than a stream of presses.
|
|
2651
|
+
*
|
|
2652
|
+
* @group Input
|
|
2653
|
+
*/
|
|
2654
|
+
type InputState<Action extends string = string> = {
|
|
2655
|
+
/**
|
|
2656
|
+
* What presses each action, and what to call it in front of a player.
|
|
2657
|
+
*
|
|
2658
|
+
* `mapping.shoot.label` is the whole reason this is readable: it is *Z* on a
|
|
2659
|
+
* keyboard, a glyph on whatever pad the player picked up, and the label the
|
|
2660
|
+
* manifest gave the on-screen button on a touch screen — the name written on
|
|
2661
|
+
* whatever the player would press. So a game writes one prompt and is right
|
|
2662
|
+
* on all of them. An action can be missing, so keep a fallback.
|
|
2663
|
+
*/
|
|
2664
|
+
readonly mapping: Readonly<Partial<Record<Action, InputBinding>>>;
|
|
2665
|
+
/** What the player is using right now. */
|
|
2666
|
+
readonly scheme: InputScheme;
|
|
2667
|
+
};
|
|
2668
|
+
/**
|
|
2669
|
+
* A player rebound something, or picked up another device. `previous` says
|
|
2670
|
+
* which: a different `scheme` is a new device, a different `mapping` under the
|
|
2671
|
+
* same one is a rebinding.
|
|
2672
|
+
*
|
|
2673
|
+
* @group Input
|
|
2674
|
+
*/
|
|
2675
|
+
type InputListener<Action extends string = string> = (now: InputState<Action>, previous: InputState<Action>) => void;
|
|
2676
|
+
/**
|
|
2677
|
+
* What the player is doing, as the frame sees it: `sdk.input`.
|
|
2678
|
+
*
|
|
2679
|
+
* @group Input
|
|
2680
|
+
* @category Important
|
|
2681
|
+
*/
|
|
2682
|
+
type Input<Action extends string = string, Layout extends string = string> = InputState<Action> & {
|
|
2683
|
+
/**
|
|
2684
|
+
* Run this whenever an action goes down or comes up.
|
|
2685
|
+
*
|
|
2686
|
+
* One press however many things are bound to it: an action held by a key and
|
|
2687
|
+
* a pad button at once goes down with the first and up with the last, rather
|
|
2688
|
+
* than flickering as a player moves between them.
|
|
2689
|
+
*
|
|
2690
|
+
* An action the manifest never declared is reported to `onError`: it would
|
|
2691
|
+
* never fire.
|
|
2692
|
+
*/
|
|
2693
|
+
onAction(action: Action, listener: ActionListener<Action>): Unsubscribe;
|
|
2694
|
+
/** Run this when a stick moves. `pad.stick` hears from either of them. */
|
|
2695
|
+
onStick(stick: StickName, listener: StickListener): Unsubscribe;
|
|
2696
|
+
/** Run this when the pointer moves or its button changes. `null` is *nowhere*. */
|
|
2697
|
+
onPointer(listener: PointerListener): Unsubscribe;
|
|
2698
|
+
/**
|
|
2699
|
+
* Run this when `mapping` or `scheme` changes: a player rebinds something, or
|
|
2700
|
+
* picks up a controller. Frozen and replaced rather than mutated, so a value
|
|
2701
|
+
* read in a frame loop cannot change under it.
|
|
2702
|
+
*/
|
|
2703
|
+
on(listener: InputListener<Action>): Unsubscribe;
|
|
2704
|
+
readonly controls: Controls<Layout>;
|
|
2705
|
+
/**
|
|
2706
|
+
* Whether to keep enough state for `read()` to answer about movement.
|
|
2707
|
+
*
|
|
2708
|
+
* Presses are always tracked, so a key already held when this is switched on
|
|
2709
|
+
* is reported honestly by the first read, and a pad's buttons and sticks are
|
|
2710
|
+
* tracked from the moment the player presses something on it. What this gates
|
|
2711
|
+
* is the pointer's position, which costs a listener on every move.
|
|
2712
|
+
*/
|
|
2713
|
+
poll: boolean;
|
|
2714
|
+
/**
|
|
2715
|
+
* What is true now, and what has happened since the last read.
|
|
2716
|
+
*
|
|
2717
|
+
* Call it once at the top of a frame: taking a snapshot consumes the edges, so
|
|
2718
|
+
* a second reader in the same frame sees none of the presses the first did.
|
|
2719
|
+
* The pointer's position needs `poll` set; presses and sticks do not.
|
|
2720
|
+
*/
|
|
2721
|
+
read(): InputSnapshot<Action>;
|
|
2722
|
+
/**
|
|
2723
|
+
* Stop reading input and let go of everything held.
|
|
2724
|
+
*
|
|
2725
|
+
* For a text field a game drew itself, where the action layer would otherwise
|
|
2726
|
+
* eat the keystrokes. Ordinary inputs and `contenteditable` are stepped around
|
|
2727
|
+
* without being asked; this is for the one case that cannot be seen from
|
|
2728
|
+
* outside — a field drawn on a canvas.
|
|
2729
|
+
*/
|
|
2730
|
+
suspend(): void;
|
|
2731
|
+
resume(): void;
|
|
2732
|
+
readonly suspended: boolean;
|
|
2733
|
+
};
|
|
2734
|
+
/**
|
|
2735
|
+
* The on-screen controls: shown when the game says so *and* the host says they
|
|
2736
|
+
* apply.
|
|
2737
|
+
*
|
|
2738
|
+
* Two halves, and a game needs both answers. Asking is the game's — a pad drawn
|
|
2739
|
+
* over a title screen is worse than one that arrives a moment late — and whether
|
|
2740
|
+
* there is any point is the host's, because a desktop with a keyboard has none.
|
|
2741
|
+
*
|
|
2742
|
+
* @group Input
|
|
2743
|
+
* @category Useful
|
|
2744
|
+
*/
|
|
2745
|
+
type Controls<Layout extends string = string> = {
|
|
2746
|
+
/**
|
|
2747
|
+
* Draw a layout, if this host will have one.
|
|
2748
|
+
*
|
|
2749
|
+
* The name may be left out by a game that declared exactly one layout. Showing
|
|
2750
|
+
* a different layout while a button is held releases that button, because a
|
|
2751
|
+
* control that has gone away cannot be the thing that ends its own press. A
|
|
2752
|
+
* layout the manifest never declared is reported to `onError`.
|
|
2753
|
+
*/
|
|
2754
|
+
show(layout?: Layout): void;
|
|
2755
|
+
/**
|
|
2756
|
+
* Take them down, releasing anything they were holding.
|
|
2757
|
+
*
|
|
2758
|
+
* Hiding is about drawing and nothing else: the keys and pad buttons bound to
|
|
2759
|
+
* the same actions go on working.
|
|
2760
|
+
*/
|
|
2761
|
+
hide(): void;
|
|
2762
|
+
/**
|
|
2763
|
+
* Whether controls are on the screen right now.
|
|
2764
|
+
*
|
|
2765
|
+
* The effective answer, not the request: the game asked, *and* the host allows
|
|
2766
|
+
* it, *and* the layout fitted on this frame. A game draws its own touch hints
|
|
2767
|
+
* off this rather than off having called `show`.
|
|
2768
|
+
*/
|
|
2769
|
+
readonly showing: boolean;
|
|
2770
|
+
/** The layout the game last asked for, or `null`. */
|
|
2771
|
+
readonly asked: Layout | null;
|
|
2772
|
+
/** Whether this host has any use for on-screen controls at all. */
|
|
2773
|
+
readonly available: boolean;
|
|
2774
|
+
};
|
|
2775
|
+
//#endregion
|
|
2776
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/lifecycle.d.ts
|
|
2777
|
+
/** @group Lifecycle */
|
|
2778
|
+
type LifecycleListener = (phase: Phase, previous: Phase) => void | Promise<void>;
|
|
2779
|
+
/**
|
|
2780
|
+
* @group Lifecycle
|
|
2781
|
+
* @category Important
|
|
2782
|
+
*/
|
|
2783
|
+
type Lifecycle = {
|
|
2784
|
+
/** How far this game has got. `loading` until it says otherwise. */
|
|
2785
|
+
readonly phase: Phase;
|
|
2786
|
+
/**
|
|
2787
|
+
* Tell the host the game can be shown. Nothing to wait for: `phase` becomes
|
|
2788
|
+
* `ready` once the host has heard, and a failure is reported to `onError`.
|
|
2789
|
+
*/
|
|
2790
|
+
ready(): void;
|
|
2791
|
+
/** Runs on every change of phase. Return a promise to hold the reply. */
|
|
2792
|
+
on(listener: LifecycleListener): Unsubscribe;
|
|
2793
|
+
};
|
|
2794
|
+
//#endregion
|
|
2795
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/loop.d.ts
|
|
2796
|
+
/**
|
|
2797
|
+
* The host has let the game run its loop, or stopped it. Return a promise to
|
|
2798
|
+
* make the host wait.
|
|
2799
|
+
*
|
|
2800
|
+
* @group Loop
|
|
2801
|
+
*/
|
|
2802
|
+
type LoopListener = (allowed: boolean) => void | Promise<void>;
|
|
2803
|
+
/**
|
|
2804
|
+
* One frame of the game's loop: `dt` is the seconds since the last frame,
|
|
2805
|
+
* capped at `MAX_STEP` so a hitch never becomes one enormous step, and 0 on the
|
|
2806
|
+
* first frame after the loop starts or comes back. `now` is the frame's own
|
|
2807
|
+
* timestamp, in milliseconds.
|
|
2808
|
+
*
|
|
2809
|
+
* @group Loop
|
|
2810
|
+
*/
|
|
2811
|
+
type FrameCallback = (dt: number, now: number) => void;
|
|
2812
|
+
/**
|
|
2813
|
+
* The longest step `tick` hands a frame, in seconds.
|
|
2814
|
+
*
|
|
2815
|
+
* @group Loop
|
|
2816
|
+
*/
|
|
2817
|
+
declare const MAX_STEP = 0.1;
|
|
2818
|
+
/**
|
|
2819
|
+
* @group Loop
|
|
2820
|
+
* @category Important
|
|
2821
|
+
*/
|
|
2822
|
+
type Loop = {
|
|
2823
|
+
/**
|
|
2824
|
+
* Whether the game may ask for frames now. Known as soon as `init` resolves.
|
|
2825
|
+
*
|
|
2826
|
+
* `false` means no `requestAnimationFrame`, no timers driving the game: what
|
|
2827
|
+
* was drawn last stays on screen.
|
|
2828
|
+
*/
|
|
2829
|
+
readonly allowed: boolean;
|
|
2830
|
+
/**
|
|
2831
|
+
* Told whenever `allowed` changes. Return a promise to hold the host's reply.
|
|
2832
|
+
*
|
|
2833
|
+
* For a game whose engine runs its own loop: sleep it when this says `false`
|
|
2834
|
+
* and wake it when it says `true`.
|
|
2835
|
+
*/
|
|
2836
|
+
on(listener: LoopListener): Unsubscribe;
|
|
2837
|
+
/**
|
|
2838
|
+
* Run `frame` once per display frame while the loop is `allowed`, and not at
|
|
2839
|
+
* all otherwise. What it drew last stays on screen meanwhile.
|
|
2840
|
+
*
|
|
2841
|
+
* A player's own pause is the game's: the loop keeps running so the game can
|
|
2842
|
+
* draw its pause screen, and the game stops advancing its own clock.
|
|
2843
|
+
*
|
|
2844
|
+
* Returns a function that stops this frame callback for good.
|
|
2845
|
+
*/
|
|
2846
|
+
tick(frame: FrameCallback): Unsubscribe;
|
|
2847
|
+
};
|
|
2848
|
+
//#endregion
|
|
2849
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/panel.d.ts
|
|
2850
|
+
/**
|
|
2851
|
+
* Where the host's panel stands over the game.
|
|
2852
|
+
*
|
|
2853
|
+
* @group Panels
|
|
2854
|
+
*/
|
|
2855
|
+
type PanelState = {
|
|
2856
|
+
/**
|
|
2857
|
+
* Whether the panel is over the game, on any page: the game's own, or one of
|
|
2858
|
+
* the host's, such as the player's own tab. Either way the player is reading
|
|
2859
|
+
* the panel rather than playing.
|
|
2860
|
+
*/
|
|
2861
|
+
readonly isOpen: boolean;
|
|
2862
|
+
/**
|
|
2863
|
+
* Which of the game's pages it is open on, or `null`: closed, or open on a
|
|
2864
|
+
* page of the host's own that a game has no name for.
|
|
2865
|
+
*/
|
|
2866
|
+
readonly page: PageId | null;
|
|
2867
|
+
};
|
|
2868
|
+
/**
|
|
2869
|
+
* The panel opened, closed or moved to another page.
|
|
2870
|
+
*
|
|
2871
|
+
* `previous` is where it stood before, so a listener can tell *closed* from
|
|
2872
|
+
* *moved* without keeping its own copy — a player going from Scores to Input
|
|
2873
|
+
* never passes through a closed panel, and a game that resumed on every change
|
|
2874
|
+
* would resume mid-read.
|
|
2875
|
+
*
|
|
2876
|
+
* @group Panels
|
|
2877
|
+
*/
|
|
2878
|
+
type PanelListener = (now: PanelState, previous: PanelState) => void;
|
|
2879
|
+
/**
|
|
2880
|
+
* @group Panels
|
|
2881
|
+
* @category Important
|
|
2882
|
+
*/
|
|
2883
|
+
type Panel = {
|
|
2884
|
+
/**
|
|
2885
|
+
* Whether this host will open its panel for this game at all: `pages` is not
|
|
2886
|
+
* empty.
|
|
2887
|
+
*/
|
|
2888
|
+
readonly available: boolean;
|
|
2889
|
+
/**
|
|
2890
|
+
* The pages this host will open for this game, in the order it would show
|
|
2891
|
+
* them.
|
|
2892
|
+
*
|
|
2893
|
+
* Empty is a host with no panel and a host older than panels alike, which
|
|
2894
|
+
* are one fact from here: draw it yourself. It is per game as well as per
|
|
2895
|
+
* host — a game with no boards is not offered `scores`, because there is no
|
|
2896
|
+
* board for the host to draw. A page a host names that this SDK has no word
|
|
2897
|
+
* for is left out: a game cannot open what it cannot name.
|
|
2898
|
+
*/
|
|
2899
|
+
readonly pages: readonly PageId[];
|
|
2900
|
+
/**
|
|
2901
|
+
* Whether the panel is over the game right now, on any page.
|
|
2902
|
+
*
|
|
2903
|
+
* Right from the first frame: a host does not close its own furniture to
|
|
2904
|
+
* start a game — the site's panel follows the focused game rather than
|
|
2905
|
+
* shutting on travel — so this is answered at the handshake rather than after
|
|
2906
|
+
* the first change.
|
|
2907
|
+
*
|
|
2908
|
+
* Reading it in a loop is free; it changes only when the host says so.
|
|
2909
|
+
*/
|
|
2910
|
+
readonly isOpen: boolean;
|
|
2911
|
+
/**
|
|
2912
|
+
* Which of the game's pages the panel is open on, or `null`: closed, or open
|
|
2913
|
+
* on a page of the host's own.
|
|
2914
|
+
*
|
|
2915
|
+
* **Not restricted to `pages`.** A host may show a page of the game's that no
|
|
2916
|
+
* game was ever invited to open, and a game that ignored it because the name
|
|
2917
|
+
* was unfamiliar would keep playing under it.
|
|
2918
|
+
*/
|
|
2919
|
+
readonly page: PageId | null;
|
|
2920
|
+
/**
|
|
2921
|
+
* Run this whenever the panel opens, closes or moves to another page.
|
|
2922
|
+
*
|
|
2923
|
+
* Returns a function that removes the listener. Whatever a listener throws
|
|
2924
|
+
* goes to `init`'s `onError` and the rest still run: one game's broken
|
|
2925
|
+
* handler must not stop the others hearing that the panel closed.
|
|
2926
|
+
*
|
|
2927
|
+
* **This is the callback, and `layout` is the mechanism.** A game that only
|
|
2928
|
+
* needs to move its buttons out of the way should keep using `layout` — the
|
|
2929
|
+
* rectangle is the answer to that question. This is for the decisions a
|
|
2930
|
+
* rectangle cannot support: ducking the music while they read, holding a run
|
|
2931
|
+
* rather than counting it against them.
|
|
2932
|
+
*
|
|
2933
|
+
* It is not a pause signal: the panel opens over a game that is still
|
|
2934
|
+
* `active`. A game that says it is `running` is asked to pause through
|
|
2935
|
+
* `pause` when the panel opens over it; for anything else, what to do about
|
|
2936
|
+
* it is the game's.
|
|
2937
|
+
*/
|
|
2938
|
+
on(listener: PanelListener): Unsubscribe;
|
|
2939
|
+
/**
|
|
2940
|
+
* Ask the host to open its panel on one of its pages, in front of the player.
|
|
2941
|
+
*
|
|
2942
|
+
* Nothing to wait for: when the player closes it is not something to wait
|
|
2943
|
+
* on either, because a host is free to keep its panel docked beside the game
|
|
2944
|
+
* and never close it. What happens arrives through `on`.
|
|
2945
|
+
*
|
|
2946
|
+
* A page this host does not draw does nothing — a game checking `pages` first
|
|
2947
|
+
* never asks for one. Anything else that goes wrong is reported to `onError`:
|
|
2948
|
+
* `rate_limited` is a host that will not open it right now, asked too often
|
|
2949
|
+
* or asked to cover a player mid-run.
|
|
2950
|
+
*
|
|
2951
|
+
* **Call it from a press, and only from a press.** This puts something over
|
|
2952
|
+
* the game, and a host is within its rights to refuse one no player asked
|
|
2953
|
+
* for — the same rule a share is under, and for the same reason.
|
|
2954
|
+
*/
|
|
2955
|
+
open(page: PageId): void;
|
|
2956
|
+
};
|
|
2957
|
+
//#endregion
|
|
2958
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/pause.d.ts
|
|
2959
|
+
/**
|
|
2960
|
+
* The host has asked for a pause, or for the game to carry on.
|
|
2961
|
+
*
|
|
2962
|
+
* **Return a promise and the host waits for it**, the way it waits on an
|
|
2963
|
+
* order: a game that has to finish an animation before it
|
|
2964
|
+
* can honestly say it has stopped holds the reply until it has. The leash is
|
|
2965
|
+
* short — half a second at most hosts — so slow is fine and hung is not.
|
|
2966
|
+
*
|
|
2967
|
+
* **Say where you ended up with `set`.** The reply the host receives is
|
|
2968
|
+
* whatever the state is once every listener has settled, so a listener that
|
|
2969
|
+
* pauses the game calls `set('paused')` and one that finds there is nothing to
|
|
2970
|
+
* pause does nothing at all: the state is already `unpausable`, and that is the
|
|
2971
|
+
* honest answer.
|
|
2972
|
+
*
|
|
2973
|
+
* @group Pause
|
|
2974
|
+
*/
|
|
2975
|
+
type PauseListener = (paused: boolean) => void | Promise<void>;
|
|
2976
|
+
/**
|
|
2977
|
+
* @group Pause
|
|
2978
|
+
* @category Important
|
|
2979
|
+
*/
|
|
2980
|
+
type Pause = {
|
|
2981
|
+
/**
|
|
2982
|
+
* Where this game currently stands, as the host has last been told.
|
|
2983
|
+
*
|
|
2984
|
+
* `unpausable` until the game says otherwise, which is what a game that never
|
|
2985
|
+
* mentions pausing means.
|
|
2986
|
+
*/
|
|
2987
|
+
readonly state: PauseState;
|
|
2988
|
+
/**
|
|
2989
|
+
* Say where the game now stands.
|
|
2990
|
+
*
|
|
2991
|
+
* **The one way to change it, whoever's idea it was.** A player pressing the
|
|
2992
|
+
* game's own pause control, a run ending onto a title screen with nothing
|
|
2993
|
+
* left to stop, and a listener applying what the host just asked for all end
|
|
2994
|
+
* up here — so there is one place a game's pause state is written and one
|
|
2995
|
+
* message shape it reaches the host in.
|
|
2996
|
+
*
|
|
2997
|
+
* Told to the host immediately, unless it is what the host already believes:
|
|
2998
|
+
* saying the same word twice is not a change, and a host that redrew its
|
|
2999
|
+
* control on every frame of a game that calls this in its loop would be a
|
|
3000
|
+
* host flickering for nothing.
|
|
3001
|
+
*
|
|
3002
|
+
* Nothing is stopped by calling this. Freezing the clock, holding the
|
|
3003
|
+
* animation and quieting the sound are the game's own work, and this is how
|
|
3004
|
+
* the game says it has done it.
|
|
3005
|
+
*/
|
|
3006
|
+
set(state: PauseState): void;
|
|
3007
|
+
/**
|
|
3008
|
+
* Told whenever the host asks for a pause or a resume: its own pause button,
|
|
3009
|
+
* its panel opening, or the game being taken away from the player mid-play.
|
|
3010
|
+
*
|
|
3011
|
+
* It hears the host asking, not the state moving — the state is whatever
|
|
3012
|
+
* the game says with `set` — so it is not called for a change the game made
|
|
3013
|
+
* itself: a listener that fired on the game's own `set` would be a game
|
|
3014
|
+
* hearing its own decision back, and one that answered it would be a loop.
|
|
3015
|
+
*/
|
|
3016
|
+
on(listener: PauseListener): Unsubscribe;
|
|
3017
|
+
};
|
|
3018
|
+
//#endregion
|
|
3019
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/resources.d.ts
|
|
3020
|
+
/**
|
|
3021
|
+
* The host has let the game hold its resources again, or asked for them back.
|
|
3022
|
+
* Return a promise to make the host wait until they have gone, or come back.
|
|
3023
|
+
*
|
|
3024
|
+
* @group Resources
|
|
3025
|
+
*/
|
|
3026
|
+
type ResourcesListener = (allowed: boolean) => void | Promise<void>;
|
|
3027
|
+
/**
|
|
3028
|
+
* @group Resources
|
|
3029
|
+
* @category Important
|
|
3030
|
+
*/
|
|
3031
|
+
type Resources = {
|
|
3032
|
+
/**
|
|
3033
|
+
* Whether the game may hold what it could load again. Known as soon as
|
|
3034
|
+
* `init` resolves.
|
|
3035
|
+
*
|
|
3036
|
+
* `false` means let go of whatever is large and can be rebuilt — textures,
|
|
3037
|
+
* decoded sound, the level's geometry — and keep what cannot, such as the
|
|
3038
|
+
* state of play.
|
|
3039
|
+
*/
|
|
3040
|
+
readonly allowed: boolean;
|
|
3041
|
+
/**
|
|
3042
|
+
* Told whenever `allowed` changes. Return a promise to hold the host's
|
|
3043
|
+
* reply: the host waits, briefly, for the memory to have gone before it
|
|
3044
|
+
* warms the next game.
|
|
3045
|
+
*/
|
|
3046
|
+
on(listener: ResourcesListener): Unsubscribe;
|
|
3047
|
+
};
|
|
3048
|
+
//#endregion
|
|
3049
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/save.d.ts
|
|
3050
|
+
/**
|
|
3051
|
+
* What a game gets back from a read: its own schema number, and its state.
|
|
3052
|
+
*
|
|
3053
|
+
* @group Save
|
|
3054
|
+
* @category Useful
|
|
3055
|
+
*/
|
|
3056
|
+
type SaveState = {
|
|
3057
|
+
/** Whatever the game passed to `write`, or 1 where it never named one. */
|
|
3058
|
+
version: number;
|
|
3059
|
+
/** Parsed where it was written as a value, verbatim where it was a string. */
|
|
3060
|
+
state: unknown;
|
|
3061
|
+
};
|
|
3062
|
+
/**
|
|
3063
|
+
* What is stored, as far as anybody has said.
|
|
3064
|
+
*
|
|
3065
|
+
* `known: false` is nobody having said yet — a host whose store had not
|
|
3066
|
+
* answered by the handshake — and the only way to find out is to `read()`.
|
|
3067
|
+
* `known: true` carries the answer: a header (how big, how old, what schema),
|
|
3068
|
+
* or `null` for a player with nothing stored. The two are kept apart by a
|
|
3069
|
+
* field a game has to look at, rather than by `null` against `undefined`.
|
|
3070
|
+
*
|
|
3071
|
+
* @group Save
|
|
3072
|
+
* @category Useful
|
|
3073
|
+
*/
|
|
3074
|
+
type SaveStatus = {
|
|
3075
|
+
readonly known: false;
|
|
3076
|
+
} | {
|
|
3077
|
+
readonly known: true;
|
|
3078
|
+
readonly stored: SaveStored | null;
|
|
3079
|
+
};
|
|
3080
|
+
/**
|
|
3081
|
+
* Told when what is stored changes: a read answered what was unknown, or the
|
|
3082
|
+
* game wrote or cleared its save.
|
|
3083
|
+
*
|
|
3084
|
+
* @group Save
|
|
3085
|
+
* @category Useful
|
|
3086
|
+
*/
|
|
3087
|
+
type SaveListener = (now: SaveStatus, previous: SaveStatus) => void;
|
|
3088
|
+
/**
|
|
3089
|
+
* @group Save
|
|
3090
|
+
* @category Important
|
|
3091
|
+
*/
|
|
3092
|
+
type Save = {
|
|
3093
|
+
/**
|
|
3094
|
+
* Whether this host keeps saves at all. Known at the handshake: a game with
|
|
3095
|
+
* none leaves out *Continue* rather than learning on the press.
|
|
3096
|
+
*/
|
|
3097
|
+
readonly available: boolean;
|
|
3098
|
+
/**
|
|
3099
|
+
* What is stored, before anything has been fetched.
|
|
3100
|
+
*
|
|
3101
|
+
* Enough to draw *Continue* or *New game* before asking for anything, and
|
|
3102
|
+
* enough for a game now on schema 3 that will not migrate a 1 to never fetch
|
|
3103
|
+
* it at all. It follows the game's own reads, writes and clears.
|
|
3104
|
+
*/
|
|
3105
|
+
readonly status: SaveStatus;
|
|
3106
|
+
/** Told whenever `status` changes. */
|
|
3107
|
+
on(listener: SaveListener): Unsubscribe;
|
|
3108
|
+
/**
|
|
3109
|
+
* Fetch the state.
|
|
3110
|
+
*
|
|
3111
|
+
* `null` for a player with nothing stored. Rejects with a `BridgeError` where
|
|
3112
|
+
* the host refused — `unsupported` from a host with no store — which is a
|
|
3113
|
+
* game's cue to carry on without one rather than to fail to load.
|
|
3114
|
+
*/
|
|
3115
|
+
read(): Promise<SaveState | null>;
|
|
3116
|
+
/**
|
|
3117
|
+
* Keep this. Resolves when the host says the record is durable.
|
|
3118
|
+
*
|
|
3119
|
+
* A string is stored as it stands and comes back a string; anything else is
|
|
3120
|
+
* `JSON.stringify`d and comes back parsed. A value that cannot survive that
|
|
3121
|
+
* at all — a cycle, a `BigInt` — rejects with `bad_params` before anything
|
|
3122
|
+
* reaches the bridge.
|
|
3123
|
+
*
|
|
3124
|
+
* `version` is the game's own schema number and means whatever the game says
|
|
3125
|
+
* it means. There is no migration hook: a hook would be frozen game-side code
|
|
3126
|
+
* that is sugar over an `if`, and a game that got it wrong could never be
|
|
3127
|
+
* fixed.
|
|
3128
|
+
*/
|
|
3129
|
+
write(state: unknown, options?: {
|
|
3130
|
+
version?: number;
|
|
3131
|
+
}): Promise<void>;
|
|
3132
|
+
/** Throw the save away — a game resetting its own progress, at a player's asking. */
|
|
3133
|
+
clear(): Promise<void>;
|
|
3134
|
+
/** The largest save this host will take, in bytes. `SAVE_BYTES` where it said nothing. */
|
|
3135
|
+
readonly maxBytes: number;
|
|
3136
|
+
};
|
|
3137
|
+
//#endregion
|
|
3138
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/score.d.ts
|
|
3139
|
+
/**
|
|
3140
|
+
* Where this player stands on every board, as far as anybody has said.
|
|
3141
|
+
*
|
|
3142
|
+
* `known: false` is this host not having an answer yet — a store still
|
|
3143
|
+
* replying, a client that cannot reach its server — and a game that drew "no
|
|
3144
|
+
* best yet" for it would be telling somebody their record was gone because a
|
|
3145
|
+
* network was slow. `known: true` carries every board this host takes, each
|
|
3146
|
+
* with the player's standing on it or `null` for a board they have never
|
|
3147
|
+
* scored on.
|
|
3148
|
+
*
|
|
3149
|
+
* @group Score
|
|
3150
|
+
* @category Useful
|
|
3151
|
+
*/
|
|
3152
|
+
type ScoreStandings<Board extends string = string> = {
|
|
3153
|
+
readonly known: false;
|
|
3154
|
+
} | {
|
|
3155
|
+
readonly known: true;
|
|
3156
|
+
readonly boards: Readonly<Record<Board, ScoreStanding | null>>;
|
|
3157
|
+
};
|
|
3158
|
+
/**
|
|
3159
|
+
* Told when the standing changes: the host's answer arrived after the
|
|
3160
|
+
* handshake, or a submission moved it.
|
|
3161
|
+
*
|
|
3162
|
+
* @group Score
|
|
3163
|
+
* @category Useful
|
|
3164
|
+
*/
|
|
3165
|
+
type ScoreListener<Board extends string = string> = (now: ScoreStandings<Board>, previous: ScoreStandings<Board>) => void;
|
|
3166
|
+
/**
|
|
3167
|
+
* @group Score
|
|
3168
|
+
* @category Important
|
|
3169
|
+
*/
|
|
3170
|
+
type Score<Board extends string = string> = {
|
|
3171
|
+
/**
|
|
3172
|
+
* Whether this host keeps scores at all. Known at the handshake: a game with
|
|
3173
|
+
* none leaves out its score screen rather than learning on the first
|
|
3174
|
+
* submission.
|
|
3175
|
+
*/
|
|
3176
|
+
readonly available: boolean;
|
|
3177
|
+
/**
|
|
3178
|
+
* The boards this host will take a score on, from the manifest it holds.
|
|
3179
|
+
* Empty where it keeps none.
|
|
3180
|
+
*/
|
|
3181
|
+
readonly boards: readonly Board[];
|
|
3182
|
+
/** Where this player stands on each board. */
|
|
3183
|
+
readonly standing: ScoreStandings<Board>;
|
|
3184
|
+
/** Told whenever `standing` changes. */
|
|
3185
|
+
on(listener: ScoreListener<Board>): Unsubscribe;
|
|
3186
|
+
/**
|
|
3187
|
+
* Keep this number, and tell me what it did.
|
|
3188
|
+
*
|
|
3189
|
+
* Resolves with `improved` — whether this run beat the player's own best,
|
|
3190
|
+
* where a tie is not an improvement — and `best`, what that best now is.
|
|
3191
|
+
* `rank` and `of` come too when the host happens to know them, and a game
|
|
3192
|
+
* that draws a position must handle their being absent: a host that
|
|
3193
|
+
* acknowledges a score as soon as it is *durable* has not necessarily heard
|
|
3194
|
+
* back from anywhere yet.
|
|
3195
|
+
*
|
|
3196
|
+
* `board` may be left out by a game with one board, which is what a game with
|
|
3197
|
+
* one board should do. A game with several that leaves it out is refused
|
|
3198
|
+
* rather than guessed at.
|
|
3199
|
+
*
|
|
3200
|
+
* Rejects with `unsupported` from a host that keeps no scores. `bad_params`
|
|
3201
|
+
* is a board this game's manifest never declared, which is an author's
|
|
3202
|
+
* mistake and not a player's. `rate_limited` means this is being called far
|
|
3203
|
+
* more often than runs actually end.
|
|
3204
|
+
*
|
|
3205
|
+
* **Every run is its own submission.** A worse run than last time is not an
|
|
3206
|
+
* error and is not to be withheld — the host keeps it, `improved` comes back
|
|
3207
|
+
* `false`, and the best stands. A game that only submits its personal bests
|
|
3208
|
+
* is a game whose weekly board has one number in it.
|
|
3209
|
+
*/
|
|
3210
|
+
submit(value: number, options?: {
|
|
3211
|
+
board?: Board;
|
|
3212
|
+
}): Promise<ScoreResult>;
|
|
3213
|
+
};
|
|
3214
|
+
//#endregion
|
|
3215
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/share.d.ts
|
|
3216
|
+
/**
|
|
3217
|
+
* @group Share
|
|
3218
|
+
* @category Important
|
|
3219
|
+
*/
|
|
3220
|
+
type Share = {
|
|
3221
|
+
/**
|
|
3222
|
+
* Whether this host can put a link in front of the player. Known at the
|
|
3223
|
+
* handshake: draw a Share button only where it is `true`, rather than
|
|
3224
|
+
* learning on the press that there was nothing behind it.
|
|
3225
|
+
*/
|
|
3226
|
+
readonly available: boolean;
|
|
3227
|
+
/**
|
|
3228
|
+
* What the link that opened this game carried, or `null` for a player who
|
|
3229
|
+
* arrived the ordinary way.
|
|
3230
|
+
*
|
|
3231
|
+
* Exactly the string some run of this game passed to `offer` — the board that
|
|
3232
|
+
* was sent, the seed that was beaten — and it is here at startup rather than
|
|
3233
|
+
* fetched, so a game opened from a link draws what the link says in its first
|
|
3234
|
+
* frame instead of drawing the ordinary game and jumping.
|
|
3235
|
+
*
|
|
3236
|
+
* **Read it the way you would read a stranger's input, because it is one.** A
|
|
3237
|
+
* URL is written by anybody: it can be truncated by a chat client, edited by
|
|
3238
|
+
* a curious player, or made up. This game wrote the format and this game must
|
|
3239
|
+
* check it — the same care `save.read` deserves, for a stronger reason.
|
|
3240
|
+
*
|
|
3241
|
+
* **`null` says nothing about whether a link can be offered.** It means
|
|
3242
|
+
* nobody arrived by one; `available` is what says whether one can be made.
|
|
3243
|
+
*/
|
|
3244
|
+
readonly data: string | null;
|
|
3245
|
+
/**
|
|
3246
|
+
* Ask the host to put a link to this game in front of the player.
|
|
3247
|
+
*
|
|
3248
|
+
* Nothing to wait for. The host puts the link in front of the player — a
|
|
3249
|
+
* share sheet, the clipboard — and tells them what became of it, "link
|
|
3250
|
+
* copied" included; the game is not told, and has nothing to draw. A player
|
|
3251
|
+
* may keep a sheet open as long as they like, and pressing Share again while
|
|
3252
|
+
* one is up does not open a second.
|
|
3253
|
+
*
|
|
3254
|
+
* `data` is the game's own and travels in the link, opaque to the host, back
|
|
3255
|
+
* to whoever opens it. A string, because it has to survive being a URL and
|
|
3256
|
+
* because the game reading it back is the game that wrote it: anything
|
|
3257
|
+
* structured is the game's to encode and the game's to parse. `SHARE_BYTES`
|
|
3258
|
+
* is what every host carries; more than that is a host's own decision, and a
|
|
3259
|
+
* host that will not carry it refuses with `too_large`.
|
|
3260
|
+
*
|
|
3261
|
+
* `meta` is for the platform rather than for the game: why this link was made
|
|
3262
|
+
* — the level, the score, which of the game's buttons — kept beside the
|
|
3263
|
+
* host's own record of who made it and how, so a game's makers can learn
|
|
3264
|
+
* which of its shares bring people in. Anything JSON can say, and
|
|
3265
|
+
* `SHARE_META_BYTES` of it is what every host takes. Nobody who opens the link
|
|
3266
|
+
* is handed it.
|
|
3267
|
+
*
|
|
3268
|
+
* What goes wrong is reported to `onError` and sends nothing: `bad_params`
|
|
3269
|
+
* for a `data` that is not a string or a `meta` JSON cannot carry, which are
|
|
3270
|
+
* bugs in the game; `too_large` from a host for more than it carries;
|
|
3271
|
+
* `rate_limited` from one that will not offer a link right now. A host with
|
|
3272
|
+
* no link to give does nothing, which a game checking `available` never asks
|
|
3273
|
+
* of it. Every one of them is a game that carries on: nothing about a share
|
|
3274
|
+
* belongs to the state of play.
|
|
3275
|
+
*
|
|
3276
|
+
* **Call it from a press, and only from a press.** This opens something over
|
|
3277
|
+
* the game, and a host will be within its rights to refuse an offer no player
|
|
3278
|
+
* asked for — some cannot even honour one, because the permission to put a
|
|
3279
|
+
* sheet on screen comes from the player's own gesture.
|
|
3280
|
+
*/
|
|
3281
|
+
offer(options?: {
|
|
3282
|
+
data?: string;
|
|
3283
|
+
meta?: ShareMeta;
|
|
3284
|
+
}): void;
|
|
3285
|
+
};
|
|
3286
|
+
//#endregion
|
|
3287
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/init.d.ts
|
|
3288
|
+
/**
|
|
3289
|
+
* What `init` hands back: every namespace a game has, on one object.
|
|
3290
|
+
*
|
|
3291
|
+
* `M` is the game's manifest type, `typeof config.manifest`: every name the
|
|
3292
|
+
* game passes in — an action, a board, an achievement, an event, a channel —
|
|
3293
|
+
* is then one its manifest declares. Without it, every name is a `string`.
|
|
3294
|
+
*
|
|
3295
|
+
* `Useful` rather than `Important` because of how it is reached. A reader comes
|
|
3296
|
+
* looking for `init` — it is the only way in — and arrives here by following
|
|
3297
|
+
* what it returns, which is the same path the code takes.
|
|
3298
|
+
*
|
|
3299
|
+
* @group Bridge
|
|
3300
|
+
* @category Useful
|
|
3301
|
+
*/
|
|
3302
|
+
type GameSdk<M extends object = GameManifest> = {
|
|
3303
|
+
/**
|
|
3304
|
+
* How far the game has got: `loading`, `ready`, `disposed`. The one thing
|
|
3305
|
+
* here a game must actually call is `lifecycle.ready()`.
|
|
3306
|
+
*
|
|
3307
|
+
* @category Important
|
|
3308
|
+
*/
|
|
3309
|
+
lifecycle: Lifecycle;
|
|
3310
|
+
/**
|
|
3311
|
+
* The game's frame loop, and the host's order about it: ask for frames only
|
|
3312
|
+
* while `loop.allowed`. `loop.tick()` runs a frame callback that obeys it on
|
|
3313
|
+
* the game's behalf.
|
|
3314
|
+
*
|
|
3315
|
+
* @category Important
|
|
3316
|
+
*/
|
|
3317
|
+
loop: Loop;
|
|
3318
|
+
/**
|
|
3319
|
+
* The host's order about memory: hold what can be loaded again only while
|
|
3320
|
+
* `resources.allowed`, and let it go when told.
|
|
3321
|
+
*
|
|
3322
|
+
* @category Important
|
|
3323
|
+
*/
|
|
3324
|
+
resources: Resources;
|
|
3325
|
+
/**
|
|
3326
|
+
* What the host has put over the frame, how big that frame is, and what the
|
|
3327
|
+
* two leave the game. Numbers only: what to do about them is the game's, and
|
|
3328
|
+
* so is whatever it draws with.
|
|
3329
|
+
*
|
|
3330
|
+
* @category Useful
|
|
3331
|
+
*/
|
|
3332
|
+
layout: Layout;
|
|
3333
|
+
/**
|
|
3334
|
+
* Who this run is for and what it is running on: the host, the game as the
|
|
3335
|
+
* host loaded it, the player, the machine.
|
|
3336
|
+
*
|
|
3337
|
+
* Settled at the handshake and frozen. There is nothing to listen to because
|
|
3338
|
+
* there is nothing that changes — which is also why reading it in a loop is
|
|
3339
|
+
* free, and why a game can branch on the device tier before it fetches its
|
|
3340
|
+
* first asset.
|
|
3341
|
+
*
|
|
3342
|
+
* @category Useful
|
|
3343
|
+
*/
|
|
3344
|
+
info: Info;
|
|
3345
|
+
/**
|
|
3346
|
+
* Say what happened, for the host to keep.
|
|
3347
|
+
*
|
|
3348
|
+
* One way and unacknowledged: nothing here can be waited on, and nothing here
|
|
3349
|
+
* can fail. What the host does with an event — store it, show it in a log,
|
|
3350
|
+
* throw it away — is no business of the game's.
|
|
3351
|
+
*
|
|
3352
|
+
* @category Useful
|
|
3353
|
+
*/
|
|
3354
|
+
analytics: Analytics<EventProps<M>>;
|
|
3355
|
+
/**
|
|
3356
|
+
* The one thing a game may keep: one document, this player's, this game's.
|
|
3357
|
+
*
|
|
3358
|
+
* Read it while you load and write it as you play. `save.status` says what
|
|
3359
|
+
* is waiting before anything is fetched, so a game can draw *Continue*
|
|
3360
|
+
* without a round trip; `read()` fetches it, `write()` resolves when the host
|
|
3361
|
+
* says the record is durable, and there is nothing to flush because nothing
|
|
3362
|
+
* is held here.
|
|
3363
|
+
*
|
|
3364
|
+
* `save.available` is `false` on a host with no store, whose calls answer
|
|
3365
|
+
* `unsupported`: a game with no save rather than one that fails to load.
|
|
3366
|
+
*
|
|
3367
|
+
* @category Useful
|
|
3368
|
+
*/
|
|
3369
|
+
save: Save;
|
|
3370
|
+
/**
|
|
3371
|
+
* Keep a number against this player, and be told what it did.
|
|
3372
|
+
*
|
|
3373
|
+
* `score.standing` says where they already are before anything is asked
|
|
3374
|
+
* for, so a game draws a target in its first frame; `submit()` resolves with
|
|
3375
|
+
* whether the run beat it. What a game never gets is anybody else's number —
|
|
3376
|
+
* a board is drawn by the client over the game, not inside it.
|
|
3377
|
+
*
|
|
3378
|
+
* `score.available` is `false` on a host with no scoring, whose calls answer
|
|
3379
|
+
* `unsupported`: a game that carries on without a target.
|
|
3380
|
+
*
|
|
3381
|
+
* @category Useful
|
|
3382
|
+
*/
|
|
3383
|
+
score: Score<BoardName<M>>;
|
|
3384
|
+
/**
|
|
3385
|
+
* What this player has been shown to have done, and how to say they have
|
|
3386
|
+
* done more.
|
|
3387
|
+
*
|
|
3388
|
+
* `achievements.standing` answers before anything is asked for, so a game
|
|
3389
|
+
* draws *3 of 12* in its first frame; `unlock()` says something is theirs
|
|
3390
|
+
* and `progress()` says how far along they are — as an absolute value, so
|
|
3391
|
+
* reporting it twice does nothing. What a game never gets is anybody else's:
|
|
3392
|
+
* the panel that lists them is drawn by the client over the game, not inside
|
|
3393
|
+
* it.
|
|
3394
|
+
*
|
|
3395
|
+
* `achievements.available` is `false` on a host with none, and reporting to
|
|
3396
|
+
* it does nothing: a game that carries on without a banner.
|
|
3397
|
+
*
|
|
3398
|
+
* @category Useful
|
|
3399
|
+
*/
|
|
3400
|
+
achievements: Achievements<AchievementName<M>, MeasuredName<M>>;
|
|
3401
|
+
/**
|
|
3402
|
+
* What the player presses, how far they are pushing a stick, where the
|
|
3403
|
+
* pointer is — and the on-screen controls to draw where there is no keyboard.
|
|
3404
|
+
*
|
|
3405
|
+
* Read in the frame and answered in the frame: nothing about a press crosses
|
|
3406
|
+
* the bridge, and the controls are drawn here so a thumb on a stick reaches
|
|
3407
|
+
* the game in the frame it moved. What the host contributes is what the frame
|
|
3408
|
+
* cannot see — what this player remapped, what they are holding, and whether
|
|
3409
|
+
* on-screen controls mean anything on this device.
|
|
3410
|
+
*
|
|
3411
|
+
* The actions and the on-screen layouts are the ones the host read from the
|
|
3412
|
+
* published manifest, sent at the handshake: a game declares them in its
|
|
3413
|
+
* manifest and nowhere else. A host older than input sends none, and a game
|
|
3414
|
+
* there has nothing to bind.
|
|
3415
|
+
*
|
|
3416
|
+
* @category Useful
|
|
3417
|
+
*/
|
|
3418
|
+
input: Input<ActionName<M>, LayoutName<M>>;
|
|
3419
|
+
/**
|
|
3420
|
+
* How loud this game may be, which the host decides and the game obeys.
|
|
3421
|
+
*
|
|
3422
|
+
* **The one namespace here that is not a service and cannot be refused.**
|
|
3423
|
+
* Everything else is a game asking a host for something; this is a host
|
|
3424
|
+
* telling a game something, and respecting it is the condition of being on
|
|
3425
|
+
* the platform rather than a courtesy. `output()` and `media()` obey it on
|
|
3426
|
+
* the game's behalf — the volume, mute, and silence whenever sound is not
|
|
3427
|
+
* `allowed`. `volume(channel)` is the number to multiply a sound by, for a
|
|
3428
|
+
* game whose engine mixes for itself.
|
|
3429
|
+
*
|
|
3430
|
+
* A host older than audio, or one with no volume controls, leaves this at
|
|
3431
|
+
* full and never pushes: the game plays the mix it meant, which is the only
|
|
3432
|
+
* sensible reading of silence here.
|
|
3433
|
+
*
|
|
3434
|
+
* @category Useful
|
|
3435
|
+
*/
|
|
3436
|
+
audio: Audio<ChannelName<M>>;
|
|
3437
|
+
/**
|
|
3438
|
+
* Ask the host to offer the player a link to this game, carrying whatever the
|
|
3439
|
+
* game wants carried.
|
|
3440
|
+
*
|
|
3441
|
+
* The game names no URL and is told none: where this game lives is the host's
|
|
3442
|
+
* fact, and putting a link in front of a player — a sheet, the clipboard — is
|
|
3443
|
+
* something only the host can do from outside the frame. `share.available`
|
|
3444
|
+
* says at the handshake whether this host has a link to give.
|
|
3445
|
+
*
|
|
3446
|
+
* `share.data` is the way back in: what the link this run was opened with
|
|
3447
|
+
* carried, known at the handshake so a game can draw it in its first frame.
|
|
3448
|
+
*
|
|
3449
|
+
* @category Useful
|
|
3450
|
+
*/
|
|
3451
|
+
share: Share;
|
|
3452
|
+
/**
|
|
3453
|
+
* Ask the host to open its panel on one of its pages: what this game is,
|
|
3454
|
+
* the board, the list of what has been unlocked, the page where a key is
|
|
3455
|
+
* rebound.
|
|
3456
|
+
*
|
|
3457
|
+
* The host draws all of it and the game supplies the moment — a pause screen,
|
|
3458
|
+
* the end of a run, the fourth wrong key. Nothing here says where the panel
|
|
3459
|
+
* goes or what it looks like: a page is a subject, and the shape of one is
|
|
3460
|
+
* the host's.
|
|
3461
|
+
*
|
|
3462
|
+
* `panel.pages` is what a game draws its own furniture from, known at the
|
|
3463
|
+
* handshake: a game that shows *Press ⚙ to rebind* when `input` is there
|
|
3464
|
+
* draws its own rebinding screen when it is not, and it decides that before
|
|
3465
|
+
* its first frame rather than when the player presses.
|
|
3466
|
+
*
|
|
3467
|
+
* `panel.isOpen`, `panel.page` and `panel.on` are the other direction — the
|
|
3468
|
+
* panel is over the game now, whoever opened it, and on which page. Higher
|
|
3469
|
+
* level than the `overlay` rectangle in `layout`, which is still the right
|
|
3470
|
+
* answer for moving buttons out of the way: this one says it is the panel,
|
|
3471
|
+
* and which page, so a game can duck its music while a player rebinds a key
|
|
3472
|
+
* rather than guessing from a box.
|
|
3473
|
+
*
|
|
3474
|
+
* Pausing is `pause` below, and the two are worth keeping apart: a game that
|
|
3475
|
+
* says it is `running` is asked to pause when the panel opens over it, and
|
|
3476
|
+
* that request arrives there rather than here.
|
|
3477
|
+
*
|
|
3478
|
+
* @category Useful
|
|
3479
|
+
*/
|
|
3480
|
+
panel: Panel;
|
|
3481
|
+
/**
|
|
3482
|
+
* Whether the game is holding still, which the host may ask for and only the
|
|
3483
|
+
* game can answer.
|
|
3484
|
+
*
|
|
3485
|
+
* **The one namespace where a host's request may honestly come back saying
|
|
3486
|
+
* something else.** A game with nothing on the field to stop — a title
|
|
3487
|
+
* screen, the gap between rounds — answers `unpausable`, and that is a state
|
|
3488
|
+
* the host draws rather than a failure it recovers from: it shows no control
|
|
3489
|
+
* at all, because a disabled button is a promise that pressing it would do
|
|
3490
|
+
* something.
|
|
3491
|
+
*
|
|
3492
|
+
* `on()` is where the stopping goes, and the host waits for it the way it
|
|
3493
|
+
* waits for a fade-out. The host asks when its pause button is pressed, when
|
|
3494
|
+
* its panel opens, and when it takes a game in play away from the player. `set()` is how the game says where it ended up —
|
|
3495
|
+
* including when nobody asked, which is a player pressing the game's own
|
|
3496
|
+
* pause key.
|
|
3497
|
+
*
|
|
3498
|
+
* Nothing here stops anything by itself: what freezes and what carries on is
|
|
3499
|
+
* a thousand decisions this SDK has no business making. A game that never
|
|
3500
|
+
* calls `set` stays `unpausable`, which is what a host is told about every
|
|
3501
|
+
* game published before pausing existed.
|
|
3502
|
+
*
|
|
3503
|
+
* @category Useful
|
|
3504
|
+
*/
|
|
3505
|
+
pause: Pause;
|
|
3506
|
+
/**
|
|
3507
|
+
* Ask the host to answer, and time how long it takes.
|
|
3508
|
+
*
|
|
3509
|
+
* Resolves with the round trip in milliseconds, rejecting with a `BridgeError`
|
|
3510
|
+
* whose code is `timeout` if the host never answers. This is the liveness
|
|
3511
|
+
* check the demo is built on, and it stays useful afterwards.
|
|
3512
|
+
*/
|
|
3513
|
+
ping(): Promise<number>;
|
|
3514
|
+
/** Tear the bridge down. Calls in flight reject with `disposed`. */
|
|
3515
|
+
dispose(): void;
|
|
3516
|
+
};
|
|
3517
|
+
/**
|
|
3518
|
+
* @group Bridge
|
|
3519
|
+
* @category Useful
|
|
3520
|
+
*/
|
|
3521
|
+
type InitOptions = {
|
|
3522
|
+
/** The window the game is running in. Defaults to the global one. */
|
|
3523
|
+
scope?: Window | undefined;
|
|
3524
|
+
/**
|
|
3525
|
+
* How long a call waits before rejecting with `timeout`.
|
|
3526
|
+
*
|
|
3527
|
+
* Never shorter for the handshake than ten seconds, whatever this says. A
|
|
3528
|
+
* game that gives up on a hello the host has heard is one the host goes on to
|
|
3529
|
+
* show anyway, once it tires of waiting for `ready` — with nothing running
|
|
3530
|
+
* in it.
|
|
3531
|
+
*/
|
|
3532
|
+
timeoutMs?: number | undefined;
|
|
3533
|
+
/**
|
|
3534
|
+
* The channel to talk over. Defaults to whatever the game turns out to be
|
|
3535
|
+
* running in — a shim a native shell left, or the frame that embedded us —
|
|
3536
|
+
* which is what every real game wants; a test, or a host that embeds a game
|
|
3537
|
+
* some other way, can hand over its own.
|
|
3538
|
+
*/
|
|
3539
|
+
transport?: Transport | undefined;
|
|
3540
|
+
/**
|
|
3541
|
+
* How long to wait for a host to turn up when there is neither a shim nor a
|
|
3542
|
+
* parent frame, before rejecting. Defaults to `SHIM_WAIT_MS`.
|
|
3543
|
+
*
|
|
3544
|
+
* Here from day one rather than when a platform first needs it: this is
|
|
3545
|
+
* game-side code, bundled into a game at build time, so it can never be added
|
|
3546
|
+
* later for games already published.
|
|
3547
|
+
*/
|
|
3548
|
+
shimWaitMs?: number | undefined;
|
|
3549
|
+
/**
|
|
3550
|
+
* Where a failure that belongs to nobody's promise goes — a lifecycle
|
|
3551
|
+
* listener that threw, an event the SDK refused. Defaults to `console.error`.
|
|
3552
|
+
*
|
|
3553
|
+
* The host is told about each of these as well, whatever this does with
|
|
3554
|
+
* them, as it is about any error the game throws and nothing catches.
|
|
3555
|
+
*/
|
|
3556
|
+
onError?: ((error: unknown) => void) | undefined;
|
|
3557
|
+
};
|
|
3558
|
+
/**
|
|
3559
|
+
* Introduce the game to its host, and hand back the SDK once it has answered.
|
|
3560
|
+
*
|
|
3561
|
+
* The handshake is not optional and `init` does not resolve without it. It
|
|
3562
|
+
* settles the protocol version both sides will speak, and gets this game a
|
|
3563
|
+
* name to stamp its requests with — without which a host cannot tell what a
|
|
3564
|
+
* reloaded game says from what the game before it said.
|
|
3565
|
+
*
|
|
3566
|
+
* **Let a rejection through rather than catching it.** Every way this fails
|
|
3567
|
+
* leaves the game with no host to talk to — none there, one that never
|
|
3568
|
+
* answered, one too old or too new to speak to it — and a host does not show a
|
|
3569
|
+
* game that never got through the handshake. Nobody is left to read a message
|
|
3570
|
+
* drawn into the page; a developer reads the console, which is where an
|
|
3571
|
+
* uncaught rejection goes, with its stack. Nor is there a way to carry on
|
|
3572
|
+
* without a host: that is every namespace written twice, and the half no host
|
|
3573
|
+
* ever runs is the half that breaks.
|
|
3574
|
+
*
|
|
3575
|
+
* Calling it twice in one game is a bug: the host only ever talks to the most
|
|
3576
|
+
* recent one, and the abandoned peer's calls start failing with `stale`.
|
|
3577
|
+
*
|
|
3578
|
+
* **The window is sized by the time it resolves.** `innerWidth` and
|
|
3579
|
+
* `innerHeight` agree with `layout.frame` from then on, so a game sizes itself
|
|
3580
|
+
* the way any page does.
|
|
3581
|
+
*
|
|
3582
|
+
* Pass the manifest's type to have every declared name checked:
|
|
3583
|
+
*
|
|
3584
|
+
* import type config from '../hopple.config.ts';
|
|
3585
|
+
* const hopple = await init<typeof config.manifest>();
|
|
3586
|
+
*
|
|
3587
|
+
* @group Bridge
|
|
3588
|
+
* @category Important
|
|
3589
|
+
*/
|
|
3590
|
+
declare function init<M extends object = GameManifest>(options?: InitOptions): Promise<GameSdk<M>>;
|
|
3591
|
+
//#endregion
|
|
3592
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/transport.d.ts
|
|
3593
|
+
/**
|
|
3594
|
+
* How long a game with no shim and no parent waits for one to turn up.
|
|
3595
|
+
*
|
|
3596
|
+
* Generous on purpose: a shim two seconds late is not coming, and the only game
|
|
3597
|
+
* made to wait is one that was never going to run.
|
|
3598
|
+
*
|
|
3599
|
+
* @group Bridge
|
|
3600
|
+
*/
|
|
3601
|
+
declare const SHIM_WAIT_MS = 2000;
|
|
3602
|
+
/** @group Bridge */
|
|
3603
|
+
type HostTransportOptions = {
|
|
3604
|
+
/** The window the game is running in. Defaults to the global one. */
|
|
3605
|
+
scope?: Window | undefined;
|
|
3606
|
+
/** How long to wait for a late shim before giving up. */
|
|
3607
|
+
shimWaitMs?: number | undefined;
|
|
3608
|
+
/** Where a shim that arrived wrongly, or twice, is reported. */
|
|
3609
|
+
onError?: ((error: unknown) => void) | undefined;
|
|
3610
|
+
};
|
|
3611
|
+
/**
|
|
3612
|
+
* Find something to talk over, waiting briefly if there is nothing yet.
|
|
3613
|
+
*
|
|
3614
|
+
* Asynchronous because of that last case alone. The two ordinary ones — a shim
|
|
3615
|
+
* already installed, a parent frame — resolve without yielding a turn.
|
|
3616
|
+
*
|
|
3617
|
+
* @group Bridge
|
|
3618
|
+
* @category Useful
|
|
3619
|
+
*/
|
|
3620
|
+
declare function hostTransport(options?: HostTransportOptions): Promise<Transport>;
|
|
3621
|
+
//#endregion
|
|
3622
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-1wJl1B/game/src/shim.d.ts
|
|
3623
|
+
/**
|
|
3624
|
+
* Whether an arbitrary value is a shim this SDK knows how to speak to.
|
|
3625
|
+
*
|
|
3626
|
+
* A version we do not know is a shim we must not guess at — a `v: 2` that moved
|
|
3627
|
+
* `send` to a different signature would be a channel we corrupt rather than one
|
|
3628
|
+
* we fail on. Refusing it falls through to the frame branch, and then to the
|
|
3629
|
+
* wait, which either connects or says clearly that nothing did.
|
|
3630
|
+
*
|
|
3631
|
+
* @group Bridge
|
|
3632
|
+
*/
|
|
3633
|
+
declare function isShim(value: unknown): value is HoppleShim;
|
|
3634
|
+
/**
|
|
3635
|
+
* What a platform left for us, or nothing — in which case we are in a frame.
|
|
3636
|
+
*
|
|
3637
|
+
* @group Bridge
|
|
3638
|
+
*/
|
|
3639
|
+
declare function findShim(scope: Window): HoppleShim | null;
|
|
3640
|
+
/**
|
|
3641
|
+
* A shim, seen by the peer as an ordinary channel.
|
|
3642
|
+
*
|
|
3643
|
+
* The trust boundary is in here, in SDK code, and never in the platform's: a
|
|
3644
|
+
* shim that cannot validate is a shim that cannot validate wrongly.
|
|
3645
|
+
*
|
|
3646
|
+
* @group Bridge
|
|
3647
|
+
*/
|
|
3648
|
+
declare function shimTransport(shim: HoppleShim): Transport;
|
|
3649
|
+
//#endregion
|
|
3650
|
+
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 };
|