@hopple/sdk 1.0.0-next.1 → 1.0.0-next.5
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 +116 -21
- package/index.d.ts +221 -104
- package/index.js +1 -1
- package/manifest.d.ts +97 -1
- package/manifest.js +59 -4
- package/manifest.md +60 -17
- package/package.json +5 -2
- package/config.schema.json +0 -954
package/README.md
CHANGED
|
@@ -99,8 +99,10 @@ relative, as above: Hopple serves a game at the root of its own origin, and a ho
|
|
|
99
99
|
load the runtime first, or that carries a copy of the SDK.
|
|
100
100
|
|
|
101
101
|
A `hopple.config.json` can point its `"$schema"` at
|
|
102
|
-
`./node_modules
|
|
103
|
-
`hopple.config.ts` is checked by its type,
|
|
102
|
+
`./node_modules/hopple/config.schema.json` for an editor to check it; a
|
|
103
|
+
`hopple.config.ts` is checked by its type,
|
|
104
|
+
`import type { HoppleConfig } from 'hopple'`. Both are the CLI's, which reads
|
|
105
|
+
the config.
|
|
104
106
|
|
|
105
107
|
```ts
|
|
106
108
|
import { init } from '@hopple/sdk';
|
|
@@ -359,6 +361,7 @@ sdk.save.available; // false where this host keeps no
|
|
|
359
361
|
sdk.save.status; // what is stored, before you fetch it (see below)
|
|
360
362
|
sdk.save.on((now, previous) => redraw(now)); // status changed: a read, a write or a clear
|
|
361
363
|
const saved = await sdk.save.read(); // { version, state } | null
|
|
364
|
+
sdk.save.onChange((now, why) => restore(now)); // the save changed under the game: play on from this
|
|
362
365
|
await sdk.save.write(state, { version: 2 }); // resolves once the host says it is safely stored
|
|
363
366
|
await sdk.save.clear();
|
|
364
367
|
sdk.save.maxBytes; // the largest save this host accepts
|
|
@@ -375,6 +378,47 @@ sdk.save.maxBytes; // the largest save this host acce
|
|
|
375
378
|
|
|
376
379
|
It follows the game's own reads, writes and clears.
|
|
377
380
|
|
|
381
|
+
**Two ways to load.** A signed-in player's save comes from a server, which
|
|
382
|
+
may take a moment:
|
|
383
|
+
|
|
384
|
+
- **Wait for it:** `await sdk.save.read()` before the first frame. The host
|
|
385
|
+
waits for the server (a few seconds at most, and not at all offline) and
|
|
386
|
+
answers with the player's save.
|
|
387
|
+
- **Start at once:** listen with `onChange`, call `read()` without awaiting
|
|
388
|
+
it, and apply whatever either hands over. The game plays from nothing, or
|
|
389
|
+
from this device's save, and takes the player's save when it comes.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const restore = (s: SaveState | null) => { progress = s ? (s.state as Progress) : fresh(); };
|
|
393
|
+
sdk.save.onChange(restore); // before read()
|
|
394
|
+
sdk.save.read().then(restore);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Listen with `onChange` either way.** The save can also change while the
|
|
398
|
+
game plays: the server answered later than the host would wait, or a page left
|
|
399
|
+
open hears that another device played on. `onChange(save, why)` hands the game
|
|
400
|
+
the save to play on from then, and why:
|
|
401
|
+
|
|
402
|
+
| `why` | What happened |
|
|
403
|
+
|---|---|
|
|
404
|
+
| `'arrived'` | Another device's save, built on what the game had. With a `merge`, merged with what the game last saved. |
|
|
405
|
+
| `'merged'` | Two saves that went their own ways, made one by the game's `merge`. |
|
|
406
|
+
| `'gone'` | Thrown away on another device, with nothing made here since. `save` is `null`. |
|
|
407
|
+
|
|
408
|
+
To keep the game's own instead (progress it hasn't saved yet, or a New game
|
|
409
|
+
the player chose a moment before a save arrived), write it from the listener:
|
|
410
|
+
it takes the handed save's place.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
sdk.save.onChange((now, why) => {
|
|
414
|
+
if (why === 'arrived' && justStartedNewGame) void sdk.save.write(progress); // the New game wins
|
|
415
|
+
else restore(now);
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
A game that does not listen is never written over: its next write is kept
|
|
420
|
+
beside the save that arrived, for its `merge` or the player to settle.
|
|
421
|
+
|
|
378
422
|
How to use it:
|
|
379
423
|
|
|
380
424
|
- **Save as you go, not only at the end.** Nothing saves on your behalf, and a
|
|
@@ -384,13 +428,18 @@ How to use it:
|
|
|
384
428
|
- **`version` is your own save format number.** It is stored next to the data,
|
|
385
429
|
so you can check it before deciding whether to read an old save. There is no
|
|
386
430
|
migration hook: write the `if` yourself.
|
|
387
|
-
- **Strings are stored as they are
|
|
388
|
-
`JSON.stringify`.**
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
- **Strings are stored as they are, and a `Uint8Array` as its bytes; anything
|
|
432
|
+
else goes through `JSON.stringify`.** Each comes back as it went in: a
|
|
433
|
+
string, a `Uint8Array`, or the parsed value. So a `Map`, a `Date` or a typed
|
|
434
|
+
array inside an object won't come back the way it went in. Inside
|
|
435
|
+
`hopple dev`, the SDK warns (via `onError`) when you save something JSON
|
|
436
|
+
would change.
|
|
437
|
+
- **Bytes pack a big save small.** Write a `Uint8Array` with
|
|
438
|
+
[`@hopple/bytes`](https://www.npmjs.com/package/@hopple/bytes) (varints, floats, strings, and flags a
|
|
439
|
+
bit each), and it is stored and counted as those bytes.
|
|
440
|
+
- **Every host accepts at least 64 KiB** (`SAVE_BYTES`), counted in UTF-8 for
|
|
441
|
+
a string or JSON and in bytes for a `Uint8Array`. A larger save is rejected
|
|
442
|
+
with `too_large` before it is sent.
|
|
394
443
|
|
|
395
444
|
**Two devices.** A signed-in player's save follows them to every device. When
|
|
396
445
|
two devices each saved without seeing the other's progress (one was offline),
|
|
@@ -403,19 +452,23 @@ const sdk = await init({
|
|
|
403
452
|
merge: (mine, theirs) => ({ version: 2, state: combine(mine.state, theirs.state) }),
|
|
404
453
|
},
|
|
405
454
|
});
|
|
406
|
-
sdk.save.
|
|
455
|
+
sdk.save.onChange((now) => restore(now)); // merged while playing: play on from this
|
|
407
456
|
```
|
|
408
457
|
|
|
409
458
|
- `merge(mine, theirs)` is pure and answers with the save to keep in place of
|
|
410
459
|
both. Throw where the two can't be merged, and the host asks the player
|
|
411
460
|
which to keep, as it does for a game with no `merge`.
|
|
412
461
|
- Asked while the game loads, the merge is what `read()` resolves with. Asked
|
|
413
|
-
while it plays, it goes to `
|
|
462
|
+
while it plays, it goes to `onChange` listeners, and waits for the next load
|
|
414
463
|
where there are none.
|
|
464
|
+
- A save that arrives while the game plays is merged with what the game last
|
|
465
|
+
saved too, where it has a `merge`, before `onChange` hears it.
|
|
466
|
+
- Inside `hopple dev`, the SDK warns (via `onError`) when the save changed
|
|
467
|
+
while nothing listened with `onChange`.
|
|
415
468
|
|
|
416
469
|
| Error | Meaning |
|
|
417
470
|
|---|---|
|
|
418
|
-
| `unsupported` | This host has nowhere to store saves (`available` is `false`)
|
|
471
|
+
| `unsupported` | This host has nowhere to store saves (`available` is `false`), or is too old to keep a `Uint8Array`. |
|
|
419
472
|
| `too_large` | Over `sdk.save.maxBytes`. |
|
|
420
473
|
| `rate_limited` | Saving too often, usually from inside the game loop. |
|
|
421
474
|
| `bad_params` | The state can't be turned into JSON at all (a cycle, a `BigInt`). |
|
|
@@ -496,7 +549,8 @@ sdk.share.offer({
|
|
|
496
549
|
meta: { level: 3, from: 'results-screen' }, // kept by the platform, never handed back
|
|
497
550
|
});
|
|
498
551
|
|
|
499
|
-
sdk.share.data; // the
|
|
552
|
+
sdk.share.data; // the string from the link that opened this run, or null
|
|
553
|
+
sdk.share.bytes; // the Uint8Array from the link that opened this run, or null
|
|
500
554
|
sdk.share.available; // whether this host can make links at all
|
|
501
555
|
```
|
|
502
556
|
|
|
@@ -511,16 +565,21 @@ sdk.share.available; // whether this host can make links at all
|
|
|
511
565
|
- **Show your Share button from `sdk.share.available`**, never from
|
|
512
566
|
`sdk.share.data`: `null` there only means this player didn't arrive by a
|
|
513
567
|
link.
|
|
514
|
-
- **Treat `sdk.share.data` as untrusted input.**
|
|
515
|
-
game. Check
|
|
516
|
-
|
|
517
|
-
|
|
568
|
+
- **Treat `sdk.share.data` and `sdk.share.bytes` as untrusted input.** They
|
|
569
|
+
come from outside your game. Check them as strictly as you would any user
|
|
570
|
+
input.
|
|
571
|
+
- `data` is a string, which comes back as `sdk.share.data`, or a `Uint8Array`,
|
|
572
|
+
which comes back as `sdk.share.bytes`. Either way it is at most 512 bytes
|
|
573
|
+
(`SHARE_BYTES`) on every host. Bytes pack a board or a replay far tighter
|
|
574
|
+
than text: see [`@hopple/bytes`](https://www.npmjs.com/package/@hopple/bytes). Put anything bigger in
|
|
575
|
+
your save.
|
|
518
576
|
- `meta` is any JSON object, at most 1024 bytes of JSON (`SHARE_META_BYTES`) on
|
|
519
577
|
every host. No game ever reads it back.
|
|
520
578
|
|
|
521
579
|
Errors go to `onError`: `too_large`, `rate_limited` (the host won't offer a
|
|
522
|
-
link right now), `bad_params` (`data` isn't a string
|
|
523
|
-
object)
|
|
580
|
+
link right now), `bad_params` (`data` isn't a string or a `Uint8Array`, or
|
|
581
|
+
`meta` isn't a JSON object), `unsupported` (bytes, on a host too old to carry
|
|
582
|
+
them). On a host that can't make links, `offer` does nothing.
|
|
524
583
|
|
|
525
584
|
The website and the dev shell support sharing.
|
|
526
585
|
|
|
@@ -673,8 +732,44 @@ sdk.input.controls.showing; // whether controls are actually visible
|
|
|
673
732
|
- Hiding them only stops drawing them; keys and gamepads keep working.
|
|
674
733
|
- While shown, they appear in `sdk.layout.obstructions` as reserved
|
|
675
734
|
rectangles, so code that avoids the host's buttons avoids them too.
|
|
676
|
-
- They are drawn in a shadow root inside your frame, so
|
|
677
|
-
|
|
735
|
+
- They are drawn in a shadow root inside your frame, so they don't appear in
|
|
736
|
+
your DOM and your CSS only reaches them where it is meant to: through
|
|
737
|
+
`hopple-controls::part(…)` and the `--hopple-pad-*` properties (see
|
|
738
|
+
[Styling them](#styling-the-on-screen-controls)).
|
|
739
|
+
|
|
740
|
+
### Styling the on-screen controls
|
|
741
|
+
|
|
742
|
+
```css
|
|
743
|
+
hopple-controls {
|
|
744
|
+
--hopple-pad-tint: #2b1d0ecc; /* a button's fill */
|
|
745
|
+
--hopple-pad-edge: #f5c542; /* rims, the knob */
|
|
746
|
+
--hopple-pad-ink: #fff8e1; /* labels and icons */
|
|
747
|
+
--hopple-pad-radius: 14px;
|
|
748
|
+
--hopple-pad-font: 700 16px/1 'My Font', sans-serif, hopple-keys;
|
|
749
|
+
}
|
|
750
|
+
hopple-controls::part(face pressed) { translate: 0 2px; }
|
|
751
|
+
hopple-controls::part(face action-jump) { background: url(ui/jump.png) center / 70% no-repeat; }
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
sdk.input.icon('jump', 'M12 3 4 13h5v8h6v-8h5z'); // a filled path in a 24×24 box, or null
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- Parts: `face`, `label` and `icon` on a button; `group` behind a group of
|
|
759
|
+
buttons; `well`, `knob`, `groove` and `arrow` on a stick. Each also carries
|
|
760
|
+
`action-<id>` and `control-<id>`, and the states `pressed`, `held`,
|
|
761
|
+
`floating` and `resting`.
|
|
762
|
+
- `<hopple-controls>` has `layout`, `sticks` and `look` attributes, and a
|
|
763
|
+
stick has `--hopple-stick-x` and `--hopple-stick-y` (−1 to 1) for a game to
|
|
764
|
+
read.
|
|
765
|
+
- Sizes (`--hopple-pad-size`, `-stick`, `-track`, `-gap`, `-inset`) are held
|
|
766
|
+
between a size a thumb can press and one that leaves the game room. How big
|
|
767
|
+
one control is next to the others is its `scale` in the manifest. What a
|
|
768
|
+
part draws never changes where a touch lands.
|
|
769
|
+
- A player can turn a game's look off, for every game or for one, and gets the
|
|
770
|
+
plain controls back: test that they still read well over the game.
|
|
771
|
+
- Under `hopple dev`, a rule that names a part or a property the controls don't
|
|
772
|
+
have is reported to `onError`.
|
|
678
773
|
|
|
679
774
|
### Several players on one device
|
|
680
775
|
|