@hopple/sdk 1.0.0-next.4 → 1.0.0-next.9

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 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/@hopple/sdk/config.schema.json` for an editor to check it; a
103
- `hopple.config.ts` is checked by its type, `HoppleConfig`.
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; anything else goes through
388
- `JSON.stringify`.** So a `Map`, a `Date` or a typed array won't come back
389
- the way it went in. Inside `hopple dev`, the SDK warns (via `onError`) when
390
- you save something JSON would change. For binary data, encode it as a
391
- base64 string yourself.
392
- - **Every host accepts at least 64 KiB** (`SAVE_BYTES`). A larger save is
393
- rejected with `too_large` before it is sent.
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.onMerge((merged) => restore(merged.state)); // merged while playing: play on from this
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 `onMerge` listeners, and waits for the next load
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 data from the link that opened this run, or null
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.** It comes from outside your
515
- game. Check it as strictly as you would any user input.
516
- - `data` must be a string, at most 512 bytes (`SHARE_BYTES`) on every host.
517
- Put anything bigger in your save.
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, or `meta` isn't a JSON
523
- object). On a host that can't make links, `offer` does nothing.
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