@hopple/sdk 0.1.0-rc.3 → 1.0.0-next.12
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/README.md +220 -44
- package/index.d.ts +1093 -879
- package/index.js +2 -29
- package/manifest.d.ts +1107 -0
- package/manifest.js +906 -0
- package/manifest.md +134 -27
- package/package.json +9 -2
- package/config.schema.json +0 -784
package/manifest.d.ts
ADDED
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
//#region ../../../../../../../tmp/hopple-sdk-types-hbSZtA/manifest/src/manifest.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The API a game is written against: `M.m`, a major and the least minor it
|
|
4
|
+
* needs, or `next`, the pre-releases of the next stable release.
|
|
5
|
+
*
|
|
6
|
+
* @group Manifest
|
|
7
|
+
* @category Important
|
|
8
|
+
* @pattern ^(?:(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)|next)$
|
|
9
|
+
* @errorMessage must be the API the game is written against, like "1.0", or "next"
|
|
10
|
+
*/
|
|
11
|
+
type ApiVersion = string;
|
|
12
|
+
/**
|
|
13
|
+
* How the game wants to be held. The host decides what to do about it.
|
|
14
|
+
*
|
|
15
|
+
* @group Layout
|
|
16
|
+
*/
|
|
17
|
+
type Orientation = 'portrait' | 'landscape' | 'any';
|
|
18
|
+
/**
|
|
19
|
+
* Whether the host may put its own furniture over the game's frame.
|
|
20
|
+
*
|
|
21
|
+
* The one layout question that has to be answered before the bundle runs, which
|
|
22
|
+
* is why it is declared here rather than asked at runtime: the host sizes the
|
|
23
|
+
* frame before there is a game to ask. A game that says nothing gets `reserve`,
|
|
24
|
+
* so a rail never lands on the buttons of something written before rails
|
|
25
|
+
* existed.
|
|
26
|
+
*
|
|
27
|
+
* @group Layout
|
|
28
|
+
*/
|
|
29
|
+
type InsetMode =
|
|
30
|
+
/** The frame is the whole surface; read `sdk.layout` and keep out of the way. */
|
|
31
|
+
'draw-under' |
|
|
32
|
+
/** The host shrinks the frame clear of its own furniture. Nothing to avoid. */
|
|
33
|
+
'reserve';
|
|
34
|
+
/**
|
|
35
|
+
* What a game declares about layout. Absent means every default.
|
|
36
|
+
*
|
|
37
|
+
* @group Layout
|
|
38
|
+
* @category Useful
|
|
39
|
+
*/
|
|
40
|
+
type LayoutDeclaration = {
|
|
41
|
+
insets: InsetMode;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* What a prop may hold, as one word. The three the wire carries, and no more.
|
|
45
|
+
*
|
|
46
|
+
* @group Analytics
|
|
47
|
+
*/
|
|
48
|
+
type AnalyticsType = 'string' | 'number' | 'boolean';
|
|
49
|
+
/**
|
|
50
|
+
* One event's props. Every one optional: see `AnalyticsDeclaration`.
|
|
51
|
+
*
|
|
52
|
+
* Prop names follow the grammar event names do, which `@hopple/protocol`
|
|
53
|
+
* enforces on the wire too; a test keeps the two in step.
|
|
54
|
+
*
|
|
55
|
+
* @group Analytics
|
|
56
|
+
* @maxProperties 8
|
|
57
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 24, errorMessage: 'must be a prop name of lowercase letters, digits, dots, hyphens and underscores' }
|
|
58
|
+
*/
|
|
59
|
+
type AnalyticsShape = Record<string, AnalyticsType>;
|
|
60
|
+
/**
|
|
61
|
+
* The custom events a game sends, and what each one carries.
|
|
62
|
+
*
|
|
63
|
+
* **The first key here whose reader is not the host.** `layout.insets` is
|
|
64
|
+
* declared because a frame has to be sized before there is a game to ask;
|
|
65
|
+
* nothing about analytics has to be answered before the bundle runs. What earns
|
|
66
|
+
* this its place is cardinality: names and columns invented at runtime make a
|
|
67
|
+
* pipe nobody can build a table for, and the party that has to build that table
|
|
68
|
+
* — the publish CLI, the server, a creator dashboard — is exactly the party that
|
|
69
|
+
* already reads the manifest and never talks to a game.
|
|
70
|
+
*
|
|
71
|
+
* **Every prop is optional and nothing is required**, which is what keeps the
|
|
72
|
+
* declaration cheap enough to be worth asking for: forgetting one costs a column
|
|
73
|
+
* rather than the event. The additive rule carries over unchanged — add events,
|
|
74
|
+
* add props, never retype one — and that is the whole of what a per-event table
|
|
75
|
+
* needs to survive the game's next version.
|
|
76
|
+
*
|
|
77
|
+
* @group Analytics
|
|
78
|
+
* @category Useful
|
|
79
|
+
*/
|
|
80
|
+
type AnalyticsDeclaration = {
|
|
81
|
+
/**
|
|
82
|
+
* Every event the game may send, by name.
|
|
83
|
+
*
|
|
84
|
+
* Names starting `hopple.` are the host's own records and not a game's to
|
|
85
|
+
* use. The grammar is the wire's too: `@hopple/protocol` enforces the same
|
|
86
|
+
* one, and a test keeps the two in step. At most 64 events: the wire never
|
|
87
|
+
* sees a declaration, so nothing enforces this at runtime and nothing has to.
|
|
88
|
+
*
|
|
89
|
+
* @maxProperties 64
|
|
90
|
+
* @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' }
|
|
91
|
+
*/
|
|
92
|
+
events: Record<string, AnalyticsShape>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* What a game expects to keep, in bytes.
|
|
96
|
+
*
|
|
97
|
+
* **The manifest's own test is whether the host must know before the bundle
|
|
98
|
+
* runs**, and here it must: a host that prefetches a save from a server has to
|
|
99
|
+
* know which games have one before there is a game to ask, and a publish
|
|
100
|
+
* pipeline that provisions storage has to know before a player exists.
|
|
101
|
+
*
|
|
102
|
+
* The budget the game expects, not a grant. A host answers with its own cap at
|
|
103
|
+
* the handshake, and a game with no key here may still call the API — it just
|
|
104
|
+
* gets nothing prefetched, which is the honest reading of not having said so.
|
|
105
|
+
*
|
|
106
|
+
* @group Save
|
|
107
|
+
* @category Useful
|
|
108
|
+
*/
|
|
109
|
+
type SaveDeclaration = {
|
|
110
|
+
/**
|
|
111
|
+
* At most 1 MiB. Not the same question as what a host will take — that is
|
|
112
|
+
* the host's and is settled at the handshake. This is only what is worth
|
|
113
|
+
* writing down: a declaration in the megabytes is a game that has
|
|
114
|
+
* misunderstood what this is for, and finding that out at publish time is
|
|
115
|
+
* cheaper than at a player's.
|
|
116
|
+
*
|
|
117
|
+
* @asType integer
|
|
118
|
+
* @exclusiveMinimum 0
|
|
119
|
+
* @maximum 1048576
|
|
120
|
+
*/
|
|
121
|
+
bytes: number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* One thing a game makes noise with, as the player is offered it.
|
|
125
|
+
*
|
|
126
|
+
* The title is the whole of it, and required: this exists to be a label on a
|
|
127
|
+
* fader in a host's own panel, and a fader labelled `sfx2` is a control nobody
|
|
128
|
+
* can use. What a channel *is* — a bus, a set of samples, one oscillator — is
|
|
129
|
+
* the game's business and is never described here.
|
|
130
|
+
*
|
|
131
|
+
* @group Audio
|
|
132
|
+
*/
|
|
133
|
+
type AudioChannel = {
|
|
134
|
+
/**
|
|
135
|
+
* What to call it in front of a player.
|
|
136
|
+
*
|
|
137
|
+
* @category Important
|
|
138
|
+
* @pattern \S
|
|
139
|
+
* @maxLength 40
|
|
140
|
+
* @errorMessage must not be blank
|
|
141
|
+
*/
|
|
142
|
+
title: string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* What this game makes noise with, so a host can offer a fader for each.
|
|
146
|
+
*
|
|
147
|
+
* **A map from the first version, even for a game with one channel.** The
|
|
148
|
+
* channel id travels in `audio.set`, which is frozen game-side code, and a
|
|
149
|
+
* single-channel key would have to *change meaning* to grow a second one — the
|
|
150
|
+
* one move the additive rule forbids.
|
|
151
|
+
*
|
|
152
|
+
* **The manifest's test is whether the host must know before the bundle runs**,
|
|
153
|
+
* and here it must twice over. The panel is drawn where the game's title is,
|
|
154
|
+
* which a host draws for games in a feed that nobody has opened; and the
|
|
155
|
+
* opening volumes ride in the handshake, so the host has to know which channels
|
|
156
|
+
* it is stating a volume for before there is a game to ask.
|
|
157
|
+
*
|
|
158
|
+
* **The key is required, and `null` is how a silent game says so.** Leaving it
|
|
159
|
+
* out would read the same as forgetting it, and a game that makes noise but
|
|
160
|
+
* never said so is one whose player cannot turn it down. An object with no
|
|
161
|
+
* channels is a game with sound and no tracks worth a fader of their own: it
|
|
162
|
+
* gets a master fader and a mute button and no more. What is never optional is
|
|
163
|
+
* obeying what the host then says: see `AudioState`.
|
|
164
|
+
*
|
|
165
|
+
* @group Audio
|
|
166
|
+
* @category Useful
|
|
167
|
+
*/
|
|
168
|
+
type AudioDeclaration = {
|
|
169
|
+
/**
|
|
170
|
+
* The channels, by id. Optional, and may be empty: either way there is only
|
|
171
|
+
* the master. At most 8: a panel of faders nobody can read is not a feature.
|
|
172
|
+
*
|
|
173
|
+
* `master` is refused rather than quietly shadowed. A master fader exists
|
|
174
|
+
* whether or not a game has said anything, and an author who wrote it meant
|
|
175
|
+
* something by it; a fader that turned out to be the host's own would be a
|
|
176
|
+
* control that ignored half of what they wired it to. The id grammar is the
|
|
177
|
+
* wire's too, and a test keeps the two in step.
|
|
178
|
+
*
|
|
179
|
+
* @maxProperties 8
|
|
180
|
+
* @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' }
|
|
181
|
+
*/
|
|
182
|
+
channels?: Record<string, AudioChannel>;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Which way a board sorts — the one thing about a score that cannot be guessed.
|
|
186
|
+
*
|
|
187
|
+
* `desc` is the ordinary leaderboard, where more is better. `asc` is a time, a
|
|
188
|
+
* stroke count, a number of moves. Nothing downstream can infer this from the
|
|
189
|
+
* values it holds, and a client that guessed would render a time board upside
|
|
190
|
+
* down, so it is required rather than defaulted.
|
|
191
|
+
*
|
|
192
|
+
* @group Score
|
|
193
|
+
*/
|
|
194
|
+
type ScoreDirection = 'asc' | 'desc';
|
|
195
|
+
/**
|
|
196
|
+
* How to draw the number.
|
|
197
|
+
*
|
|
198
|
+
* A client renders a board for a game it has never run, so it cannot ask the
|
|
199
|
+
* game what `224310` means. `integer` is a count, `decimal` a measurement,
|
|
200
|
+
* `duration-ms` a time to be written as minutes and seconds. Absent is
|
|
201
|
+
* `integer`, which is what most boards are.
|
|
202
|
+
*
|
|
203
|
+
* @group Score
|
|
204
|
+
*/
|
|
205
|
+
type ScoreFormat = 'integer' | 'decimal' | 'duration-ms';
|
|
206
|
+
/** @group Score */
|
|
207
|
+
type ScoreBoard = {
|
|
208
|
+
/**
|
|
209
|
+
* What to call it in front of a player.
|
|
210
|
+
*
|
|
211
|
+
* @category Important
|
|
212
|
+
* @pattern \S
|
|
213
|
+
* @maxLength 40
|
|
214
|
+
* @errorMessage must not be blank
|
|
215
|
+
*/
|
|
216
|
+
title: string;
|
|
217
|
+
direction: ScoreDirection;
|
|
218
|
+
/** See `ScoreFormat`. Absent means `integer`. */
|
|
219
|
+
format?: ScoreFormat;
|
|
220
|
+
/**
|
|
221
|
+
* Which board, of the ones this id has named. Absent is `1`.
|
|
222
|
+
*
|
|
223
|
+
* **Raising it starts the board again from nobody.** The runs under the
|
|
224
|
+
* version before are kept and no longer counted, shown or ranked, so a board
|
|
225
|
+
* whose scores stopped being comparable — new levels, a new rule — begins
|
|
226
|
+
* again under the id the game already submits to.
|
|
227
|
+
*
|
|
228
|
+
* **Only a change to what a stored score means needs it.** The title changes
|
|
229
|
+
* freely, and so does the format between `integer` and `decimal`. Once the
|
|
230
|
+
* board holds scores, turning it around (`direction`) or writing it in
|
|
231
|
+
* another unit (to or from `duration-ms`) is refused at the same version.
|
|
232
|
+
* Going back to a version declared before brings its runs back, so it is
|
|
233
|
+
* held to what that version was.
|
|
234
|
+
*
|
|
235
|
+
* @asType integer
|
|
236
|
+
* @minimum 1
|
|
237
|
+
* @maximum 1000
|
|
238
|
+
*/
|
|
239
|
+
version?: number;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* The boards this game keeps a score on.
|
|
243
|
+
*
|
|
244
|
+
* **A map from the first version, even for a game that wants one board.** The
|
|
245
|
+
* board id travels in `score.submit`, which is frozen game-side code, and a
|
|
246
|
+
* single-board key would have to *change meaning* to grow a second one — the
|
|
247
|
+
* one thing the additive rule forbids. A map costs a game with one board the
|
|
248
|
+
* few characters of naming it, and costs a game with four nothing at all.
|
|
249
|
+
*
|
|
250
|
+
* **Read by the host and by the server**, which is what earns it a key rather
|
|
251
|
+
* than a runtime call: a client draws a game's board in a sheet over a feed
|
|
252
|
+
* without ever having run that game, and the server has to know how a board
|
|
253
|
+
* sorts before it can answer a query about one.
|
|
254
|
+
*
|
|
255
|
+
* @group Score
|
|
256
|
+
* @category Useful
|
|
257
|
+
*/
|
|
258
|
+
type ScoreDeclaration = {
|
|
259
|
+
/**
|
|
260
|
+
* The boards, by id. At least one: a `score` key with no boards is a game
|
|
261
|
+
* that meant to declare one and did not, and reading it as "no boards" would
|
|
262
|
+
* hand it a scoreboard tab with nothing behind it. The id grammar is the
|
|
263
|
+
* wire's too, and a test keeps the two in step.
|
|
264
|
+
*
|
|
265
|
+
* @minProperties 1
|
|
266
|
+
* @maxProperties 16
|
|
267
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
268
|
+
*/
|
|
269
|
+
boards: Record<string, ScoreBoard>;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* One named step on the way to a measured achievement's target.
|
|
273
|
+
*
|
|
274
|
+
* **A tier is a label, never a second achievement.** The achievement is
|
|
275
|
+
* unlocked at its `target` and nowhere else; a tier is what a panel calls the
|
|
276
|
+
* player between here and there — *Silver, 50 of 100*. Modelling each tier as
|
|
277
|
+
* its own id would be three rows in every board that draws them and three
|
|
278
|
+
* unlocks a game has to remember to send, where this is one number reported
|
|
279
|
+
* once and read three ways.
|
|
280
|
+
*
|
|
281
|
+
* @group Achievements
|
|
282
|
+
*/
|
|
283
|
+
type AchievementTier = {
|
|
284
|
+
/**
|
|
285
|
+
* The reported value this tier is reached at, on the near side of the target.
|
|
286
|
+
*
|
|
287
|
+
* @minimum -9007199254740991
|
|
288
|
+
* @maximum 9007199254740991
|
|
289
|
+
*/
|
|
290
|
+
at: number;
|
|
291
|
+
/**
|
|
292
|
+
* What to call the player who has reached it.
|
|
293
|
+
*
|
|
294
|
+
* @category Important
|
|
295
|
+
* @pattern \S
|
|
296
|
+
* @maxLength 40
|
|
297
|
+
* @errorMessage must not be blank
|
|
298
|
+
*/
|
|
299
|
+
title: string;
|
|
300
|
+
/**
|
|
301
|
+
* A glyph for this tier, where it wants its own.
|
|
302
|
+
*
|
|
303
|
+
* @pattern \S
|
|
304
|
+
* @maxLength 8
|
|
305
|
+
* @errorMessage must not be blank
|
|
306
|
+
*/
|
|
307
|
+
icon?: string;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* What makes an achievement measured rather than flagged.
|
|
311
|
+
*
|
|
312
|
+
* **`direction` is the one thing that cannot be guessed**, exactly as it cannot
|
|
313
|
+
* be guessed for a board. `desc` — the default, and what most achievements are
|
|
314
|
+
* — counts up to the target: *break 100 blocks*. `asc` counts down to it:
|
|
315
|
+
* *finish under two minutes*, where the value kept is the player's best time
|
|
316
|
+
* and a smaller one beats a larger. A panel that assumed bigger-is-better would
|
|
317
|
+
* draw a stopwatch achievement as though every slow run were progress.
|
|
318
|
+
*
|
|
319
|
+
* **`format` is how to draw the number**, for the reason a board needs one:
|
|
320
|
+
* whoever renders this has never run the game and cannot ask it what `112400`
|
|
321
|
+
* means.
|
|
322
|
+
*
|
|
323
|
+
* Both are `ScoreDirection` and `ScoreFormat` rather than types of their own.
|
|
324
|
+
* "Which way is better" and "how is this number written" are the same two
|
|
325
|
+
* questions wherever they are asked, and a second spelling of each is two
|
|
326
|
+
* vocabularies to keep in step and one bug when they drift.
|
|
327
|
+
*
|
|
328
|
+
* @group Achievements
|
|
329
|
+
*/
|
|
330
|
+
type AchievementProgress = {
|
|
331
|
+
/**
|
|
332
|
+
* The value that unlocks it. Reaching it is the whole of being unlocked.
|
|
333
|
+
*
|
|
334
|
+
* Bounded by `Number.MAX_SAFE_INTEGER`, for the reason `SCORE_VALUE_MAX` is:
|
|
335
|
+
* past there a whole number stops round-tripping through JSON and through a
|
|
336
|
+
* 64-bit column, and progress toward a target nobody can reproduce is
|
|
337
|
+
* progress nobody can draw.
|
|
338
|
+
*
|
|
339
|
+
* @minimum -9007199254740991
|
|
340
|
+
* @maximum 9007199254740991
|
|
341
|
+
*/
|
|
342
|
+
target: number;
|
|
343
|
+
/** Which way is better. Absent is `desc`: more is better. */
|
|
344
|
+
direction?: ScoreDirection;
|
|
345
|
+
/** How to draw the value. Absent is `integer`. */
|
|
346
|
+
format?: ScoreFormat;
|
|
347
|
+
/**
|
|
348
|
+
* Named milestones on the way, ordered toward the target.
|
|
349
|
+
*
|
|
350
|
+
* @maxItems 8
|
|
351
|
+
*/
|
|
352
|
+
tiers?: AchievementTier[];
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* One thing a player can be shown to have done.
|
|
356
|
+
*
|
|
357
|
+
* **Two kinds, told apart by one key.** Without `progress` this is a flag: it
|
|
358
|
+
* happens once, and saying so twice is the same fact twice. With `progress` it
|
|
359
|
+
* is measured — a count, a distance, a best time — reported as the game goes
|
|
360
|
+
* and unlocked when the value reaches the target.
|
|
361
|
+
*
|
|
362
|
+
* **`secret` withholds the words, never the row.** A secret achievement is
|
|
363
|
+
* listed while it is locked, so a player can see there is one left, with its
|
|
364
|
+
* title and detail held back until it is theirs. Hiding the row itself would
|
|
365
|
+
* make the list's length change under the player, which reads as a bug rather
|
|
366
|
+
* than as a surprise. It is a courtesy of the panel and not a secret kept from
|
|
367
|
+
* the machine: the manifest ships beside the bundle and is public, so anybody
|
|
368
|
+
* who wants the words already has them.
|
|
369
|
+
*
|
|
370
|
+
* @group Achievements
|
|
371
|
+
*/
|
|
372
|
+
type Achievement = {
|
|
373
|
+
/**
|
|
374
|
+
* What to call it in front of a player.
|
|
375
|
+
*
|
|
376
|
+
* @category Important
|
|
377
|
+
* @pattern \S
|
|
378
|
+
* @maxLength 40
|
|
379
|
+
* @errorMessage must not be blank
|
|
380
|
+
*/
|
|
381
|
+
title: string;
|
|
382
|
+
/**
|
|
383
|
+
* One line under the title, saying what it takes.
|
|
384
|
+
*
|
|
385
|
+
* @pattern \S
|
|
386
|
+
* @maxLength 120
|
|
387
|
+
* @errorMessage must not be blank
|
|
388
|
+
*/
|
|
389
|
+
detail?: string;
|
|
390
|
+
/**
|
|
391
|
+
* A short glyph to draw beside it — an emoji, usually. Eight characters is
|
|
392
|
+
* enough for an emoji with a modifier on it.
|
|
393
|
+
*
|
|
394
|
+
* @pattern \S
|
|
395
|
+
* @maxLength 8
|
|
396
|
+
* @errorMessage must not be blank
|
|
397
|
+
*/
|
|
398
|
+
icon?: string;
|
|
399
|
+
/** Whether the words are withheld until it is unlocked. See above. */
|
|
400
|
+
secret?: boolean;
|
|
401
|
+
/** Present makes this measured rather than flagged. See `AchievementProgress`. */
|
|
402
|
+
progress?: AchievementProgress;
|
|
403
|
+
/**
|
|
404
|
+
* Which achievement, of the ones this id has named. Absent is `1`.
|
|
405
|
+
*
|
|
406
|
+
* **Raising it starts the achievement again for everybody.** What players
|
|
407
|
+
* reported and unlocked under the version before is kept and no longer
|
|
408
|
+
* counted, so it is earned afresh under the id the game already reports to.
|
|
409
|
+
*
|
|
410
|
+
* **Only a change to what a stored value means needs it.** The words change
|
|
411
|
+
* freely — title, detail, icon, secret — and so does the target, and a tier
|
|
412
|
+
* added after the last one: progress is kept as the number reported, which
|
|
413
|
+
* stays true when the goal moves, and a player who unlocked it keeps it. At
|
|
414
|
+
* the same version, a published achievement may not turn from a flag into a
|
|
415
|
+
* measure or back, turn its direction around, write its value in another
|
|
416
|
+
* unit (to or from `duration-ms`), or move or drop a tier it already has.
|
|
417
|
+
* Going back to a version declared before brings back what was earned under
|
|
418
|
+
* it, so it is held to what that version was.
|
|
419
|
+
*
|
|
420
|
+
* @asType integer
|
|
421
|
+
* @minimum 1
|
|
422
|
+
* @maximum 1000
|
|
423
|
+
*/
|
|
424
|
+
version?: number;
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* The achievements this game keeps for a player.
|
|
428
|
+
*
|
|
429
|
+
* **A map, for the reason `score.boards` is one**: the id travels in
|
|
430
|
+
* `achievement.unlock`, which is frozen game-side code, so the shape has to
|
|
431
|
+
* hold a second entry without anything changing meaning.
|
|
432
|
+
*
|
|
433
|
+
* **Read by the host, by the client and by the server, and by none of them from
|
|
434
|
+
* the game.** The site draws this panel for a game sitting in the feed that has
|
|
435
|
+
* never been run; the server refuses an id no publisher declared. That is what
|
|
436
|
+
* earns a manifest key rather than a runtime call — the same test `score`
|
|
437
|
+
* passed.
|
|
438
|
+
*
|
|
439
|
+
* @group Achievements
|
|
440
|
+
* @category Useful
|
|
441
|
+
*/
|
|
442
|
+
type AchievementDeclaration = {
|
|
443
|
+
/**
|
|
444
|
+
* The achievements, by id. At least one: an `achievements` key with nothing
|
|
445
|
+
* in it is a game that meant to declare one and did not. The id grammar is
|
|
446
|
+
* the wire's too, and a test keeps the two in step.
|
|
447
|
+
*
|
|
448
|
+
* @minProperties 1
|
|
449
|
+
* @maxProperties 128
|
|
450
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
451
|
+
*/
|
|
452
|
+
list: Record<string, Achievement>;
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* A device a game can be played with, as one word.
|
|
456
|
+
*
|
|
457
|
+
* Hardware rather than API surface: `mouse` and `touch` are separate words even
|
|
458
|
+
* though a game reads both through one pointer, because what a *client* does
|
|
459
|
+
* with this is decide whether to put a game in front of somebody who has no way
|
|
460
|
+
* to play it.
|
|
461
|
+
*
|
|
462
|
+
* @group Input
|
|
463
|
+
*/
|
|
464
|
+
type InputDevice = 'keyboard' | 'mouse' | 'touch' | 'gamepad';
|
|
465
|
+
/**
|
|
466
|
+
* One set of devices that together are enough to play.
|
|
467
|
+
*
|
|
468
|
+
* @group Input
|
|
469
|
+
* @minItems 1
|
|
470
|
+
* @maxItems 8
|
|
471
|
+
*/
|
|
472
|
+
type InputDeviceSet = InputDevice[];
|
|
473
|
+
/**
|
|
474
|
+
* A name for something that can *press* an action.
|
|
475
|
+
*
|
|
476
|
+
* `key.<code>` names a physical key by `KeyboardEvent.code` — **the code and
|
|
477
|
+
* never the character**. `KeyA` is the key where A sits on a US keyboard and
|
|
478
|
+
* stays that key on AZERTY, where it types Q, so a game that binds movement to
|
|
479
|
+
* codes has WASD under the same four fingers everywhere. `Escape` is the host's:
|
|
480
|
+
* it leaves fullscreen and pointer lock, which are invisible from inside the
|
|
481
|
+
* frame, and a game that bound it would lose that fight quietly on every press.
|
|
482
|
+
*
|
|
483
|
+
* `pad.south` is the south of either cluster: the face button on one pad and the
|
|
484
|
+
* d-pad's down on another, which is what makes one binding cover both. Compass
|
|
485
|
+
* words for both clusters, with no `pad.dpad.down` alias beside
|
|
486
|
+
* `pad.left.south` — a vocabulary that can never shrink should not start out
|
|
487
|
+
* doubled. A thumbstick is not one of these; see `InputStick`.
|
|
488
|
+
*
|
|
489
|
+
* @group Input
|
|
490
|
+
* @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\.(?:primary|secondary)|mouse\.(?:left|middle|right|wheelup|wheeldown)|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))$
|
|
491
|
+
* @errorMessage is not a binding this SDK knows (a thumbstick is read rather than bound, and Escape is the host's key)
|
|
492
|
+
*/
|
|
493
|
+
type InputBinding = string;
|
|
494
|
+
/**
|
|
495
|
+
* A name for a thumbstick — something a game reads, never binds.
|
|
496
|
+
*
|
|
497
|
+
* @group Input
|
|
498
|
+
* @pattern ^(?:controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.stick|pad\.(?:left|right)\.stick)$
|
|
499
|
+
* @errorMessage must name a thumbstick, such as pad.left.stick
|
|
500
|
+
*/
|
|
501
|
+
type InputStick = string;
|
|
502
|
+
/**
|
|
503
|
+
* What presses an action: at least one binding, and at most 8.
|
|
504
|
+
*
|
|
505
|
+
* @group Input
|
|
506
|
+
* @minItems 1
|
|
507
|
+
* @maxItems 8
|
|
508
|
+
*/
|
|
509
|
+
type InputBindings = InputBinding[];
|
|
510
|
+
/**
|
|
511
|
+
* One thing a player does, named by the game and bound to whatever produces it.
|
|
512
|
+
*
|
|
513
|
+
* **An action is a button and never an axis.** A key, a face button on a pad, a
|
|
514
|
+
* pointer button and an on-screen button all press the same action, because
|
|
515
|
+
* from the game's side they are the same event. A thumbstick is not one of
|
|
516
|
+
* those and is read through its own API — folding the two together would put a
|
|
517
|
+
* range on every binding that mostly does not have one, and would leave a game
|
|
518
|
+
* unable to say whether it meant the press or the lean.
|
|
519
|
+
*
|
|
520
|
+
* **An object rather than a bare list of bindings**, from the first version,
|
|
521
|
+
* because two strings are wanted the moment anybody draws this: a `title` for
|
|
522
|
+
* the remapping screen, which shows actions to a player and cannot show
|
|
523
|
+
* `move.left`, and a `label` for the face of an on-screen button. Growing a
|
|
524
|
+
* list into an object later would change what an action's value *is*, which is
|
|
525
|
+
* the one move the additive rule forbids. Only the object is accepted, even for
|
|
526
|
+
* an action that needs neither string: one way to write an action is one way to
|
|
527
|
+
* read it.
|
|
528
|
+
*
|
|
529
|
+
* @group Input
|
|
530
|
+
*/
|
|
531
|
+
type InputAction = {
|
|
532
|
+
/** What presses it. See `InputBinding` for the vocabulary. */
|
|
533
|
+
bind: InputBindings;
|
|
534
|
+
/**
|
|
535
|
+
* What to call it in front of a player, in a remapping screen.
|
|
536
|
+
*
|
|
537
|
+
* @pattern \S
|
|
538
|
+
* @maxLength 40
|
|
539
|
+
* @errorMessage must not be blank
|
|
540
|
+
*/
|
|
541
|
+
title?: string;
|
|
542
|
+
/**
|
|
543
|
+
* What to draw on an on-screen button: a glyph, or a letter.
|
|
544
|
+
*
|
|
545
|
+
* @pattern \S
|
|
546
|
+
* @maxLength 8
|
|
547
|
+
* @errorMessage must not be blank
|
|
548
|
+
*/
|
|
549
|
+
label?: string;
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* What presses a player's action on a pad or on the screen: a pad button, or
|
|
553
|
+
* an on-screen control. Keys and mouse buttons are a keyboard set's, in
|
|
554
|
+
* `PlayerInputDeclaration.keyboard`.
|
|
555
|
+
*
|
|
556
|
+
* @group Input
|
|
557
|
+
* @pattern ^(?:controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.(?:north|south|east|west|start|select)|pad\.(?:left|right)\.(?:north|south|east|west|bumper|trigger))$
|
|
558
|
+
* @errorMessage must be a pad button or an on-screen control: a player's keys and mouse buttons go in input.player.keyboard
|
|
559
|
+
*/
|
|
560
|
+
type PlayerBinding = string;
|
|
561
|
+
/**
|
|
562
|
+
* What presses a player's action on their part of the keyboard: a key, or a
|
|
563
|
+
* mouse button.
|
|
564
|
+
*
|
|
565
|
+
* @group Input
|
|
566
|
+
* @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\.(?:left|middle|right|wheelup|wheeldown))$
|
|
567
|
+
* @errorMessage must be a key or a mouse button (and Escape is the host's key)
|
|
568
|
+
*/
|
|
569
|
+
type KeyboardBinding = string;
|
|
570
|
+
/**
|
|
571
|
+
* One thing each player does: the same action for every local player, pressed
|
|
572
|
+
* by their own pad, their own part of the keyboard, or the on-screen controls.
|
|
573
|
+
*
|
|
574
|
+
* @group Input
|
|
575
|
+
*/
|
|
576
|
+
type PlayerInputAction = {
|
|
577
|
+
/**
|
|
578
|
+
* What presses it on a pad, the same for every pad, and on the screen.
|
|
579
|
+
* Leave it out for an action pressed only on the keyboard.
|
|
580
|
+
*
|
|
581
|
+
* @minItems 1
|
|
582
|
+
* @maxItems 8
|
|
583
|
+
*/
|
|
584
|
+
bind?: PlayerBinding[];
|
|
585
|
+
/**
|
|
586
|
+
* What to call it in front of a player, in a remapping screen.
|
|
587
|
+
*
|
|
588
|
+
* @pattern \S
|
|
589
|
+
* @maxLength 40
|
|
590
|
+
* @errorMessage must not be blank
|
|
591
|
+
*/
|
|
592
|
+
title?: string;
|
|
593
|
+
/**
|
|
594
|
+
* What to draw on an on-screen button: a glyph, or a letter.
|
|
595
|
+
*
|
|
596
|
+
* @pattern \S
|
|
597
|
+
* @maxLength 8
|
|
598
|
+
* @errorMessage must not be blank
|
|
599
|
+
*/
|
|
600
|
+
label?: string;
|
|
601
|
+
/**
|
|
602
|
+
* Whether pressing it on a pad or a part of the keyboard nobody plays with
|
|
603
|
+
* makes a new player, while the game is taking players. Left out on every
|
|
604
|
+
* action, any of them does.
|
|
605
|
+
*/
|
|
606
|
+
join?: boolean;
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* One part of the keyboard one player plays with: the keys and mouse buttons
|
|
610
|
+
* that press each of the player actions there.
|
|
611
|
+
*
|
|
612
|
+
* @group Input
|
|
613
|
+
*/
|
|
614
|
+
type KeyboardSet = {
|
|
615
|
+
/**
|
|
616
|
+
* What the players call it: *Left keys*, *Arrows*.
|
|
617
|
+
*
|
|
618
|
+
* @pattern \S
|
|
619
|
+
* @maxLength 40
|
|
620
|
+
* @errorMessage must not be blank
|
|
621
|
+
*/
|
|
622
|
+
title?: string;
|
|
623
|
+
/**
|
|
624
|
+
* The keys and mouse buttons of each player action, by its id.
|
|
625
|
+
*
|
|
626
|
+
* @maxProperties 64
|
|
627
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a player action id' }
|
|
628
|
+
*/
|
|
629
|
+
bind: Record<string, KeyboardBinding[]>;
|
|
630
|
+
};
|
|
631
|
+
/**
|
|
632
|
+
* What each local player does, and how they share the keyboard.
|
|
633
|
+
*
|
|
634
|
+
* **One set of actions for every player.** Each local player has their own
|
|
635
|
+
* copy, pressed by the pad they hold, the part of the keyboard they play with,
|
|
636
|
+
* or the on-screen controls. A pad's buttons are the same for every pad, so any
|
|
637
|
+
* number of pads just works; the keyboard is split into sets, one player each,
|
|
638
|
+
* so the number of sets is how many can share it.
|
|
639
|
+
*
|
|
640
|
+
* @group Input
|
|
641
|
+
*/
|
|
642
|
+
type PlayerInputDeclaration = {
|
|
643
|
+
/**
|
|
644
|
+
* The actions each player has, by id: never an id `actions` declares too.
|
|
645
|
+
*
|
|
646
|
+
* @maxProperties 32
|
|
647
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
648
|
+
*/
|
|
649
|
+
actions: Record<string, PlayerInputAction>;
|
|
650
|
+
/**
|
|
651
|
+
* The parts of the keyboard players play with, one player each, in the
|
|
652
|
+
* order they are offered. A player alone hears every one of them. Left out,
|
|
653
|
+
* the keyboard presses no player's action.
|
|
654
|
+
*
|
|
655
|
+
* @minItems 1
|
|
656
|
+
* @maxItems 4
|
|
657
|
+
*/
|
|
658
|
+
keyboard?: KeyboardSet[];
|
|
659
|
+
};
|
|
660
|
+
/**
|
|
661
|
+
* How a group of on-screen buttons is arranged.
|
|
662
|
+
*
|
|
663
|
+
* Arrangement only, with the count doing the rest, so three across and five
|
|
664
|
+
* down need no words of their own. **A shape nobody recognises is drawn as a
|
|
665
|
+
* `column`** — the same escape `Obstruction.reserved` is. Whoever renders these
|
|
666
|
+
* may be older than the game that declared them, and a control that fails to
|
|
667
|
+
* appear is a game that cannot be played at all. It is still refused at
|
|
668
|
+
* publish, where saying so is cheap.
|
|
669
|
+
*
|
|
670
|
+
* @group Input
|
|
671
|
+
*/
|
|
672
|
+
type ControlShape = 'row' | 'column' | 'diamond' | 'cluster';
|
|
673
|
+
/**
|
|
674
|
+
* One on-screen control: a group of buttons, or a thumbstick.
|
|
675
|
+
*
|
|
676
|
+
* **A button slot names an action; a stick slot names a stick.** The asymmetry
|
|
677
|
+
* is the hardware's. On-screen buttons have no fixed count or position — three
|
|
678
|
+
* across, five down, four in a diamond — so there is no standard control to
|
|
679
|
+
* unify them with, and the action is the only stable name they have. Thumbsticks
|
|
680
|
+
* come in twos and everybody agrees which is which, so a virtual one says which
|
|
681
|
+
* it stands for and a game reads the physical stick and this one through one id.
|
|
682
|
+
*
|
|
683
|
+
* There is no d-pad here. A d-pad is four buttons wherever it exists — four
|
|
684
|
+
* entries on the wire from a real pad, four keys on a keyboard — so a layout
|
|
685
|
+
* that wants one declares four buttons and binds them like anything else.
|
|
686
|
+
*
|
|
687
|
+
* @group Input
|
|
688
|
+
* @if { properties: { type: { const: 'buttons' } }, required: ['type'] }
|
|
689
|
+
* @then { required: ['buttons'], properties: { as: { type: 'object', additionalProperties: { $ref: '#/definitions/InputBinding' } }, axis: { not: {}, errorMessage: 'is only for a thumbstick' } } }
|
|
690
|
+
* @else { properties: { as: { $ref: '#/definitions/InputStick' } } }
|
|
691
|
+
*/
|
|
692
|
+
type ControlSlot = {
|
|
693
|
+
type: 'buttons' | 'thumbstick';
|
|
694
|
+
/**
|
|
695
|
+
* `buttons` only: the actions this control presses, in drawing order.
|
|
696
|
+
*
|
|
697
|
+
* @minItems 1
|
|
698
|
+
* @maxItems 8
|
|
699
|
+
*/
|
|
700
|
+
buttons?: string[];
|
|
701
|
+
/** `buttons` only. See `ControlShape`. Absent is `column`. */
|
|
702
|
+
shape?: ControlShape;
|
|
703
|
+
/**
|
|
704
|
+
* What this control stands in for on a standard pad.
|
|
705
|
+
*
|
|
706
|
+
* On a thumbstick it is the stick id a game reads — `pad.left.stick` — so one
|
|
707
|
+
* `onStick` sees the physical stick and this one alike. Left out, the stick is
|
|
708
|
+
* only itself, which a game reads as `controls.<id>`. On a group of buttons it
|
|
709
|
+
* is a map from action to standard button, optional and rarely wanted: a game
|
|
710
|
+
* whose bindings already name `pad.south` needs nothing here, and this is for
|
|
711
|
+
* one that also wants the low-level path to see the press.
|
|
712
|
+
*/
|
|
713
|
+
as?: InputStick | Record<string, InputBinding>;
|
|
714
|
+
/** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
|
|
715
|
+
axis?: ControlAxis;
|
|
716
|
+
/**
|
|
717
|
+
* How big this control is next to the layout's others: `1.25` for a jump
|
|
718
|
+
* button a thumb lands on without looking, `0.8` for one pressed now and then.
|
|
719
|
+
* Absent, 1.
|
|
720
|
+
*
|
|
721
|
+
* A size in the layout rather than in the game's stylesheet, because whether
|
|
722
|
+
* the layout fits on a small screen is worked out from it before the
|
|
723
|
+
* controls are drawn. A game's stylesheet changes how they look, never how
|
|
724
|
+
* much of the screen they answer in.
|
|
725
|
+
*
|
|
726
|
+
* @minimum 0.75
|
|
727
|
+
* @maximum 1.5
|
|
728
|
+
*/
|
|
729
|
+
scale?: number;
|
|
730
|
+
};
|
|
731
|
+
/**
|
|
732
|
+
* The one direction an on-screen thumbstick moves in: `x` across, `y` up and
|
|
733
|
+
* down.
|
|
734
|
+
*
|
|
735
|
+
* For a game that steers along one line — a runner sliding left and right, a
|
|
736
|
+
* paddle — where a stick that also leans up and down is a thumb wandering off
|
|
737
|
+
* the line it means. The drawn stick becomes a track along that axis and always
|
|
738
|
+
* reports the other one as 0. **Only the drawn stick:** a real pad's stick still
|
|
739
|
+
* reports both, so a game reads the axis it wants from either.
|
|
740
|
+
*
|
|
741
|
+
* Whoever draws the controls may be older than the game, and draw a stick that
|
|
742
|
+
* moves both ways. The SDK zeroes the other axis whatever the renderer reports,
|
|
743
|
+
* so the game hears one axis either way.
|
|
744
|
+
*
|
|
745
|
+
* @group Input
|
|
746
|
+
*/
|
|
747
|
+
type ControlAxis = 'x' | 'y';
|
|
748
|
+
/**
|
|
749
|
+
* The controls in one zone of a layout, by id.
|
|
750
|
+
*
|
|
751
|
+
* @group Input
|
|
752
|
+
* @maxProperties 8
|
|
753
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a control id of lowercase letters, digits, dots, hyphens and underscores' }
|
|
754
|
+
*/
|
|
755
|
+
type ControlZoneSlots = Record<string, ControlSlot>;
|
|
756
|
+
/**
|
|
757
|
+
* One named arrangement of on-screen controls.
|
|
758
|
+
*
|
|
759
|
+
* A game may declare several and switch between them — a menu wants different
|
|
760
|
+
* controls from play, and a driving section different ones again — which is why
|
|
761
|
+
* they are named rather than there being one. A zone nobody knows is a layout
|
|
762
|
+
* from a newer minor, which a host reads leniently, so whoever draws it simply
|
|
763
|
+
* has nowhere to put it.
|
|
764
|
+
*
|
|
765
|
+
* @group Input
|
|
766
|
+
*/
|
|
767
|
+
type ControlLayout = {
|
|
768
|
+
left?: ControlZoneSlots;
|
|
769
|
+
right?: ControlZoneSlots;
|
|
770
|
+
full?: ControlZoneSlots;
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* What a game says about how it is played.
|
|
774
|
+
*
|
|
775
|
+
* **It passes the manifest's test twice.** The host must know before the bundle
|
|
776
|
+
* runs — whether on-screen controls apply at all, and what a remapping screen
|
|
777
|
+
* offers, are settled before there is a game to ask — and it is read by parties
|
|
778
|
+
* that never run the game: a feed deciding whether to show a keyboard-only game
|
|
779
|
+
* to somebody holding a phone, and a remapping screen opened from outside a run.
|
|
780
|
+
*
|
|
781
|
+
* Action, control and layout ids follow the grammar the wire does, and a test
|
|
782
|
+
* keeps the two in step.
|
|
783
|
+
*
|
|
784
|
+
* @group Input
|
|
785
|
+
* @category Useful
|
|
786
|
+
*/
|
|
787
|
+
type InputDeclaration = {
|
|
788
|
+
/**
|
|
789
|
+
* The sets of hardware this game can be played with: any one set, every
|
|
790
|
+
* member of which must be present.
|
|
791
|
+
*
|
|
792
|
+
* A flat list of alternatives rather than a tree of `any` and `all`. Every
|
|
793
|
+
* real answer is a handful of alternatives, and this one renders straight into
|
|
794
|
+
* *needs a keyboard and a mouse* without anybody having written a solver.
|
|
795
|
+
*
|
|
796
|
+
* **The publisher's claim, taken at its word**, exactly as `orientation` is:
|
|
797
|
+
* nothing here checks it against the bindings. It cannot be checked. A game is
|
|
798
|
+
* free to read the platform directly — raw key events, an accelerometer, a
|
|
799
|
+
* microphone — and that is a stated allowance rather than an oversight, so
|
|
800
|
+
* what a game *binds* never implies what it can be played with.
|
|
801
|
+
*
|
|
802
|
+
* @minItems 1
|
|
803
|
+
* @maxItems 8
|
|
804
|
+
*/
|
|
805
|
+
supported?: InputDeviceSet[];
|
|
806
|
+
/**
|
|
807
|
+
* The actions, by id.
|
|
808
|
+
*
|
|
809
|
+
* @maxProperties 64
|
|
810
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be lowercase letters, digits, dots, hyphens and underscores' }
|
|
811
|
+
*/
|
|
812
|
+
actions?: Record<string, InputAction>;
|
|
813
|
+
/**
|
|
814
|
+
* Named arrangements of on-screen controls. See `ControlLayout`.
|
|
815
|
+
*
|
|
816
|
+
* @maxProperties 8
|
|
817
|
+
* @propertyNames { pattern: '^[a-z0-9]+(?:[._-][a-z0-9]+)*$', maxLength: 48, errorMessage: 'must be a layout name of lowercase letters, digits, dots, hyphens and underscores' }
|
|
818
|
+
*/
|
|
819
|
+
controls?: Record<string, ControlLayout>;
|
|
820
|
+
/**
|
|
821
|
+
* What each local player does, beside `actions`, which are everybody's. See
|
|
822
|
+
* `PlayerInputDeclaration`.
|
|
823
|
+
*/
|
|
824
|
+
player?: PlayerInputDeclaration;
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* How many people play a game on one device.
|
|
828
|
+
*
|
|
829
|
+
* Read by hosts before the game runs: a listing says *1–4 on one device*, and
|
|
830
|
+
* the input panel offers a row for each player.
|
|
831
|
+
*
|
|
832
|
+
* @group Input
|
|
833
|
+
* @category Useful
|
|
834
|
+
*/
|
|
835
|
+
type PlayersDeclaration = {
|
|
836
|
+
/**
|
|
837
|
+
* The most players on one device, each with their own copy of
|
|
838
|
+
* `input.player`'s actions. 1 when left out.
|
|
839
|
+
*
|
|
840
|
+
* @minimum 1
|
|
841
|
+
* @maximum 8
|
|
842
|
+
* @asType integer
|
|
843
|
+
*/
|
|
844
|
+
local?: number;
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* A preview's revision: a whole number from 1, the `R` of its package's
|
|
848
|
+
* `0.R.p`. A breaking change to the preview is a new revision.
|
|
849
|
+
*
|
|
850
|
+
* @group Manifest
|
|
851
|
+
* @category Useful
|
|
852
|
+
* @minimum 1
|
|
853
|
+
* @asType integer
|
|
854
|
+
*/
|
|
855
|
+
type PreviewRevision = number;
|
|
856
|
+
/**
|
|
857
|
+
* The previews a game is written against, each at the revision it needs.
|
|
858
|
+
*
|
|
859
|
+
* A preview is a part of the SDK that is not stable yet: its own `0.x`
|
|
860
|
+
* package, versioned apart from `api`, and served only to the games that
|
|
861
|
+
* declare it, at the revision they declare. A change to a preview reaches the
|
|
862
|
+
* games written against it and nobody else.
|
|
863
|
+
*
|
|
864
|
+
* @group Manifest
|
|
865
|
+
* @category Useful
|
|
866
|
+
*/
|
|
867
|
+
type PreviewsDeclaration = {
|
|
868
|
+
/**
|
|
869
|
+
* That this game plays over the network, in matches, at this revision of
|
|
870
|
+
* `@hopple/sdk-preview-network`: `sdk.match`.
|
|
871
|
+
*
|
|
872
|
+
* The permission a reviewer grants, and what lets a host offer *Play with
|
|
873
|
+
* friends* before the game has loaded. Nothing else about networking is
|
|
874
|
+
* declared: the way a match is played, and its size, are chosen by the game
|
|
875
|
+
* when it makes one. Absent, a game is never told about a match, and never
|
|
876
|
+
* reaches one.
|
|
877
|
+
*/
|
|
878
|
+
network?: PreviewRevision;
|
|
879
|
+
};
|
|
880
|
+
/**
|
|
881
|
+
* What kind of game this is, as one word from a list every game shares.
|
|
882
|
+
*
|
|
883
|
+
* A closed list, because a client files games under these and has to name each
|
|
884
|
+
* one in front of a player: left to free text, one kind of game becomes three
|
|
885
|
+
* spellings of it with a game each. The list only grows, since a published
|
|
886
|
+
* manifest has to stay valid. Whoever reads one may be older than the game, and
|
|
887
|
+
* ignores a word it does not know.
|
|
888
|
+
*
|
|
889
|
+
* `local-multiplayer` and `online-multiplayer` say how a game is played rather
|
|
890
|
+
* than what it is, and only count where the manifest bears them out: see
|
|
891
|
+
* `declaredGenres`.
|
|
892
|
+
*
|
|
893
|
+
* @group Manifest
|
|
894
|
+
* @category Useful
|
|
895
|
+
*/
|
|
896
|
+
type GameGenre = 'racing' | 'shooter' | 'platformer' | 'puzzle' | 'arcade' | 'physics' | 'roguelike' | 'strategy' | 'building' | 'idle' | 'sports' | 'horror' | 'stealth' | 'fighting' | 'casual' | 'simulation' | 'adventure' | 'rhythm' | 'rpg' | 'local-multiplayer' | 'online-multiplayer';
|
|
897
|
+
/**
|
|
898
|
+
* Publishing refuses a key the manifest's API does not know; a host reading a
|
|
899
|
+
* published one ignores it (see `validateManifest`).
|
|
900
|
+
*
|
|
901
|
+
* @group Manifest
|
|
902
|
+
* @category Important
|
|
903
|
+
*/
|
|
904
|
+
type GameManifest = {
|
|
905
|
+
/**
|
|
906
|
+
* The API the game is written against. The major picks what the game is
|
|
907
|
+
* served; the minor is the least it needs. The CLI fills it from the
|
|
908
|
+
* installed `@hopple/sdk` when the config leaves it out.
|
|
909
|
+
*
|
|
910
|
+
* @category Important
|
|
911
|
+
*/
|
|
912
|
+
api: ApiVersion;
|
|
913
|
+
/**
|
|
914
|
+
* Stable across versions, and unique among its publisher's games: it is the
|
|
915
|
+
* second half of the address the game is published at, `owner/id`, and the
|
|
916
|
+
* name every new version is filed under. Lowercase, url-safe, and no two
|
|
917
|
+
* hyphens in a row, because it ends up in urls and storage keys.
|
|
918
|
+
*
|
|
919
|
+
* @category Important
|
|
920
|
+
* @pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$
|
|
921
|
+
* @maxLength 64
|
|
922
|
+
* @errorMessage must be lowercase letters, digits and hyphens
|
|
923
|
+
*/
|
|
924
|
+
id: string;
|
|
925
|
+
/**
|
|
926
|
+
* The handle of the account the game is published under: the first half of
|
|
927
|
+
* `owner/id`. Absent, it is whoever publishes it. Present, only that account
|
|
928
|
+
* and the game's collaborators may publish it.
|
|
929
|
+
*
|
|
930
|
+
* @category Useful
|
|
931
|
+
* @pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$
|
|
932
|
+
* @maxLength 64
|
|
933
|
+
* @errorMessage must be a handle: lowercase letters, digits and hyphens
|
|
934
|
+
*/
|
|
935
|
+
owner?: string;
|
|
936
|
+
/**
|
|
937
|
+
* `false` for a game that is never to be uploaded: an example, a test, a
|
|
938
|
+
* game kept to one machine. `hopple dev` and `hopple check` still run it;
|
|
939
|
+
* `hopple upload` and `hopple publish` refuse it, and so does a server it
|
|
940
|
+
* is sent to another way. Absent, it may be published.
|
|
941
|
+
*
|
|
942
|
+
* @category Useful
|
|
943
|
+
*/
|
|
944
|
+
publishable?: boolean;
|
|
945
|
+
/**
|
|
946
|
+
* The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
|
|
947
|
+
* People see it beside the build the server numbers each upload with, and
|
|
948
|
+
* each published version needs one no other published version of the game has.
|
|
949
|
+
*
|
|
950
|
+
* @category Important
|
|
951
|
+
* @pattern ^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
|
|
952
|
+
* @errorMessage must look like 1.0.0
|
|
953
|
+
*/
|
|
954
|
+
version: string;
|
|
955
|
+
/**
|
|
956
|
+
* @category Important
|
|
957
|
+
* @pattern \S
|
|
958
|
+
* @maxLength 60
|
|
959
|
+
* @errorMessage must not be blank
|
|
960
|
+
*/
|
|
961
|
+
title: string;
|
|
962
|
+
/**
|
|
963
|
+
* A sentence or two about the game, shown beside its title.
|
|
964
|
+
*
|
|
965
|
+
* @category Useful
|
|
966
|
+
* @pattern \S
|
|
967
|
+
* @maxLength 500
|
|
968
|
+
* @errorMessage must not be blank
|
|
969
|
+
*/
|
|
970
|
+
description?: string;
|
|
971
|
+
/**
|
|
972
|
+
* What kind of game it is, the main one first. Read by whoever files it in a
|
|
973
|
+
* catalogue, which never runs it. At most `GENRE_MAX`, not counting
|
|
974
|
+
* `local-multiplayer` and `online-multiplayer`: a game that is every kind
|
|
975
|
+
* tells a player nothing. The publisher's claim, taken at its word, except
|
|
976
|
+
* for those two. See `GameGenre`.
|
|
977
|
+
*
|
|
978
|
+
* @category Useful
|
|
979
|
+
* @minItems 1
|
|
980
|
+
* @maxItems 5
|
|
981
|
+
*/
|
|
982
|
+
genres?: GameGenre[];
|
|
983
|
+
/**
|
|
984
|
+
* A picture of the game, shown wherever a link to it is pasted — a chat, a
|
|
985
|
+
* post, a message.
|
|
986
|
+
*
|
|
987
|
+
* The path of a PNG, JPEG or WebP file, relative to the config the manifest
|
|
988
|
+
* is in, and at most `COVER_MAX_BYTES`. `hopple publish` uploads it beside
|
|
989
|
+
* the game. 1200 × 630 is the shape every platform shows whole; without one,
|
|
990
|
+
* a link shows Hopple's own card.
|
|
991
|
+
*
|
|
992
|
+
* @category Useful
|
|
993
|
+
* @pattern ^(?!/)(?!.*\.\.)[A-Za-z0-9._/-]+\.(?:png|jpe?g|webp)$
|
|
994
|
+
* @maxLength 200
|
|
995
|
+
* @errorMessage must be the path of a .png, .jpg or .webp file beside the config, like "cover.png"
|
|
996
|
+
*/
|
|
997
|
+
cover?: string;
|
|
998
|
+
/** @category Important */
|
|
999
|
+
orientation: Orientation;
|
|
1000
|
+
/**
|
|
1001
|
+
* How the game wants the host's furniture handled. See `InsetMode`.
|
|
1002
|
+
*
|
|
1003
|
+
* @category Useful
|
|
1004
|
+
*/
|
|
1005
|
+
layout?: LayoutDeclaration;
|
|
1006
|
+
/**
|
|
1007
|
+
* The custom events this game sends. See `AnalyticsDeclaration`.
|
|
1008
|
+
*
|
|
1009
|
+
* @category Useful
|
|
1010
|
+
*/
|
|
1011
|
+
analytics?: AnalyticsDeclaration;
|
|
1012
|
+
/**
|
|
1013
|
+
* That this game keeps state, and roughly how much. See `SaveDeclaration`.
|
|
1014
|
+
*
|
|
1015
|
+
* @category Useful
|
|
1016
|
+
*/
|
|
1017
|
+
save?: SaveDeclaration;
|
|
1018
|
+
/**
|
|
1019
|
+
* The boards this game keeps a score on. See `ScoreDeclaration`.
|
|
1020
|
+
*
|
|
1021
|
+
* @category Useful
|
|
1022
|
+
*/
|
|
1023
|
+
score?: ScoreDeclaration;
|
|
1024
|
+
/**
|
|
1025
|
+
* What a player can be shown to have done. See `AchievementDeclaration`.
|
|
1026
|
+
*
|
|
1027
|
+
* @category Useful
|
|
1028
|
+
*/
|
|
1029
|
+
achievements?: AchievementDeclaration;
|
|
1030
|
+
/**
|
|
1031
|
+
* How this game is played, and what it draws to be played with. See `InputDeclaration`.
|
|
1032
|
+
*
|
|
1033
|
+
* @category Useful
|
|
1034
|
+
*/
|
|
1035
|
+
input?: InputDeclaration;
|
|
1036
|
+
/**
|
|
1037
|
+
* How many play on one device. See `PlayersDeclaration`.
|
|
1038
|
+
*
|
|
1039
|
+
* @category Useful
|
|
1040
|
+
*/
|
|
1041
|
+
players?: PlayersDeclaration;
|
|
1042
|
+
/**
|
|
1043
|
+
* What this game makes noise with, one channel at a time, or `null` for a
|
|
1044
|
+
* game that makes none. See `AudioDeclaration`.
|
|
1045
|
+
*
|
|
1046
|
+
* @category Important
|
|
1047
|
+
*/
|
|
1048
|
+
audio: AudioDeclaration | null;
|
|
1049
|
+
/**
|
|
1050
|
+
* The previews this game is written against, each at its revision. See
|
|
1051
|
+
* `PreviewsDeclaration`.
|
|
1052
|
+
*
|
|
1053
|
+
* @category Useful
|
|
1054
|
+
*/
|
|
1055
|
+
previews?: PreviewsDeclaration;
|
|
1056
|
+
};
|
|
1057
|
+
/**
|
|
1058
|
+
* @group Manifest
|
|
1059
|
+
* @category Useful
|
|
1060
|
+
*/
|
|
1061
|
+
type ManifestProblem = {
|
|
1062
|
+
/** The key at fault, or `''` when the document itself is the problem. */
|
|
1063
|
+
field: string;
|
|
1064
|
+
message: string;
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* @group Manifest
|
|
1068
|
+
* @category Useful
|
|
1069
|
+
*/
|
|
1070
|
+
type ManifestCheck = {
|
|
1071
|
+
ok: true;
|
|
1072
|
+
manifest: GameManifest;
|
|
1073
|
+
} | {
|
|
1074
|
+
ok: false;
|
|
1075
|
+
problems: ManifestProblem[];
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* Check a parsed manifest, without throwing and without changing it.
|
|
1079
|
+
*
|
|
1080
|
+
* Every problem is reported, not just the first: someone fixing a manifest
|
|
1081
|
+
* wants the list, and a validator that reveals one error per run is a validator
|
|
1082
|
+
* people stop running. A key the manifest does not know is refused, since a
|
|
1083
|
+
* misspelt one would otherwise mean nothing in silence; `lenient` ignores it
|
|
1084
|
+
* instead, for reading a manifest that was published, possibly against a newer
|
|
1085
|
+
* minor than this one. A key we *do* know, spelled wrongly, is refused either
|
|
1086
|
+
* way: `"draw_under"` would otherwise mean `reserve` in silence, and silence
|
|
1087
|
+
* here looks exactly like a bug in the host.
|
|
1088
|
+
*
|
|
1089
|
+
* @group Manifest
|
|
1090
|
+
* @category Important
|
|
1091
|
+
*/
|
|
1092
|
+
declare function validateManifest(value: unknown, options?: {
|
|
1093
|
+
lenient?: boolean;
|
|
1094
|
+
}): ManifestCheck;
|
|
1095
|
+
/**
|
|
1096
|
+
* One line per problem, worded the same wherever a manifest is rejected.
|
|
1097
|
+
*
|
|
1098
|
+
* Field names are set in backticks, because plenty of them are ordinary words
|
|
1099
|
+
* — "entry is required" reads as a sentence about some entry, and
|
|
1100
|
+
* "`entry` is required" names a key.
|
|
1101
|
+
*
|
|
1102
|
+
* @group Manifest
|
|
1103
|
+
* @category Useful
|
|
1104
|
+
*/
|
|
1105
|
+
declare function describeProblems(problems: readonly ManifestProblem[]): string;
|
|
1106
|
+
//#endregion
|
|
1107
|
+
export { type ManifestCheck, type ManifestProblem, describeProblems, validateManifest };
|