@arcadible/sdk 1.5.0-next.3 → 1.5.0

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
@@ -7,20 +7,26 @@
7
7
  <h1 align="center">Arcadible SDK</h1>
8
8
 
9
9
  <p align="center">
10
- Arcadible's loading screen for your game, and saves that carry over from one version to the next.
10
+ How your game works with Arcadible.
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
14
  <a href="https://developer.arcadible.com/references/arcadible-sdk">Docs</a> ·
15
15
  <a href="https://developer.arcadible.com/build/initialization">Initialization</a> ·
16
16
  <a href="https://developer.arcadible.com/build/saves">Saves</a> ·
17
+ <a href="https://developer.arcadible.com/build/pausing">Pausing</a> ·
17
18
  <a href="https://arcadible.com">Arcadible</a> ·
18
19
  <a href="https://studio.arcadible.com">Studio</a>
19
20
  </p>
20
21
 
21
22
  <br>
22
23
 
23
- The Arcadible SDK is optional: a game runs on [Arcadible](https://arcadible.com) without it. With it, a game gets two things from the play page around it: its [initialization](https://developer.arcadible.com/build/initialization), bracketed, so Arcadible's loading screen shows the game's splash and progress until its first frame is drawn, and [saves](https://developer.arcadible.com/build/saves), kept by the play page, so the player's progress carries over from one version to the next.
24
+ The Arcadible SDK is optional: a game runs on [Arcadible](https://arcadible.com) without it. With it, a game gets these from the play page around it:
25
+
26
+ - **[Initialization](https://developer.arcadible.com/build/initialization),** bracketed, so Arcadible's loading screen shows the game's splash and progress until its first frame is drawn.
27
+ - **[Saves](https://developer.arcadible.com/build/saves),** kept by the play page, so the player's progress carries over from one version to the next.
28
+ - **[Pausing](https://developer.arcadible.com/build/pausing):** Arcadible tells the game when to stop, go quiet, and save, and when to carry on.
29
+ - **[`system` and `context`](https://developer.arcadible.com/references/arcadible-sdk#system):** the store's language, and when this run launched.
24
30
 
25
31
  ## Install
26
32
 
@@ -58,11 +64,28 @@ Saves are for what your game remembers between sessions, such as progress, setti
58
64
 
59
65
  Off Arcadible, such as on your dev server, the package still works, with the same rules and limits: there's no loading screen, so `initComplete()` resolves at once, saves are kept in the game's own browser storage, `arcadible.saves.where` is `"build"`, and `arcadible.build` is `null`.
60
66
 
67
+ ## Pause and resume
68
+
69
+ ```js
70
+ arcadible.on("pause", async () => {
71
+ loop.stop();
72
+ audio.mute();
73
+ await arcadible.saves.write("progress", JSON.stringify(progress));
74
+ });
75
+ arcadible.on("resume", () => {
76
+ loop.start();
77
+ audio.unmute();
78
+ });
79
+ ```
80
+
81
+ Arcadible pauses your game when the page is hidden, when it covers your game, and before it closes or reloads it, when it waits a moment for your save. Stop, go quiet, and save, drawing nothing over your game; on `resume`, carry on, or show a "Ready?" first in an action game.
82
+
61
83
  ## Learn more
62
84
 
63
85
  - [Initialization](https://developer.arcadible.com/build/initialization): where each call goes, progress, and what comes after.
64
86
  - [Saves](https://developer.arcadible.com/build/saves): using saves well, across versions, channels, and tabs, and where they're kept.
65
- - [Arcadible SDK](https://developer.arcadible.com/references/arcadible-sdk): including it, every call, the saves' included, and every code.
87
+ - [Pausing](https://developer.arcadible.com/build/pausing): what a pause asks of your game, and resuming.
88
+ - [Arcadible SDK](https://developer.arcadible.com/references/arcadible-sdk): including it, every call, the saves' included, `system` and `context`, and every code.
66
89
  - [Limits](https://developer.arcadible.com/references/limits#saves): how many saves, and how large.
67
90
 
68
91
  ## License
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Hello } from "./bus/protocol.js";
1
+ import { type Hello } from "./bus/protocol.js";
2
2
  import { type Server } from "./bus/server.js";
3
3
  import { type ContextState } from "./context/rules.js";
4
4
  import { type Holds } from "./lifecycle/holds.js";
package/dist/host.js CHANGED
@@ -1,10 +1,12 @@
1
1
  // The page's side of the bus. The play page uses `createHost`;
2
2
  // a game standing alone runs `serve` in its own page.
3
+ // [import]
4
+ import { log } from "./bus/protocol.js";
3
5
  import { answerHellos, createServer } from "./bus/server.js";
4
6
  import { contextServer } from "./context/server.js";
5
7
  import { isTime } from "./context/rules.js";
6
8
  import { createHolds } from "./lifecycle/holds.js";
7
- import { shortLanguage } from "./system/rules.js";
9
+ import { browserLanguage } from "./system/rules.js";
8
10
  import { createSystem } from "./system/server.js";
9
11
  import { lifecycleServer } from "./lifecycle/server.js";
10
12
  import { savesServer } from "./saves/server.js";
@@ -32,6 +34,18 @@ export function serve(options) {
32
34
  });
33
35
  }
34
36
  // [function]
37
+ // The launch's time: the page's, by Arcadible's clock, or the device's when
38
+ // none is given (as under `arc serve`). One that isn't an ISO time in UTC is
39
+ // a bug in the page, said in the console, and the device's time is used, so
40
+ // the game still gets one.
41
+ function launchedAt(given) {
42
+ if (isTime(given))
43
+ return given;
44
+ if (given !== undefined) {
45
+ log("error", `launchedAt isn't an ISO time in UTC: ${JSON.stringify(given)}; the device's time is used.`);
46
+ }
47
+ return new Date().toISOString();
48
+ }
35
49
  // The play page's host: it answers its iframe's hello with a welcome and a
36
50
  // private channel. Call it before inserting the iframe, so no hello is
37
51
  // missed. It holds the game while the page is hidden; `hold` holds it for
@@ -41,7 +55,7 @@ export function createHost(options) {
41
55
  const win = options.window ?? window;
42
56
  const holds = createHolds();
43
57
  const system = createSystem({
44
- language: options.language ?? shortLanguage(win.navigator?.language),
58
+ language: options.language ?? browserLanguage(win.navigator?.language),
45
59
  });
46
60
  const answering = answerHellos({
47
61
  frame: options.frame,
@@ -53,11 +67,7 @@ export function createHost(options) {
53
67
  context: {
54
68
  // Arcadible's time when given, else the device's: never a time the
55
69
  // game would refuse.
56
- launch: {
57
- at: isTime(options.launchedAt)
58
- ? options.launchedAt
59
- : new Date().toISOString(),
60
- },
70
+ launch: { at: launchedAt(options.launchedAt) },
61
71
  },
62
72
  }),
63
73
  window: win,
@@ -1,7 +1,12 @@
1
1
  import type { Bus } from "../bus/client.js";
2
2
  /** Arcadible's state for the player, which changes while the game runs. */
3
3
  export interface System {
4
- /** The store's language, a short tag: "en", "ja". */
4
+ /**
5
+ * The language Arcadible's store shows this player: a well-formed BCP 47
6
+ * tag in its canonical form ("en", "ja", later perhaps "zh-Hant" or
7
+ * "pt-BR"), ready for `Intl`. Match it against your game's languages by
8
+ * dropping parts from the end ("zh-Hant-TW", "zh-Hant", "zh").
9
+ */
5
10
  readonly language: string;
6
11
  /**
7
12
  * Called when something changes, with the new state (this same object)
@@ -4,11 +4,19 @@ export declare const Names: {
4
4
  export type SystemState = {
5
5
  language: string;
6
6
  };
7
- /** A language the page may send: a short, lowercase tag. */
7
+ /**
8
+ * A language tag (BCP 47, as the web, iOS, and Android use) in its canonical
9
+ * form, `zh-hant-tw` as `zh-Hant-TW`, or null if it isn't a well-formed one
10
+ * (`en_US`, `English`, empty). Only its shape is checked, never whether
11
+ * Arcadible uses it, so a language added later is never refused.
12
+ */
13
+ export declare function languageOf(tag: unknown): string | null;
14
+ /** Whether a value is a language tag in its canonical form. */
8
15
  export declare function isLanguage(value: unknown): value is string;
9
- /** The state as the game keeps it: only fields it knows, each checked. */
16
+ /**
17
+ * The state as the game keeps it: only fields it knows, each checked, the
18
+ * language in its canonical form.
19
+ */
10
20
  export declare function stateOf(value: unknown): SystemState | null;
11
- /** A language tag as a short one: `en-US` as `en`, or null if it isn't one. */
12
- export declare function languageOf(tag: unknown): string | null;
13
- /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
14
- export declare function shortLanguage(tag: string | undefined): string;
21
+ /** A browser's language (`navigator.language`) as the page sends it, or `en`. */
22
+ export declare function browserLanguage(tag: string | undefined): string;
@@ -8,30 +8,54 @@ export const Names = {
8
8
  // every change.
9
9
  State: "system.state",
10
10
  };
11
- // a language as the store names it: a short tag, `en` or `ja`.
12
- const LANGUAGE = /^[a-z]{2,3}$/u;
11
+ // The longest language tag kept: room for a language, script, region, and a
12
+ // variant or two (`zh-Hant-TW`, `sl-rozaj-biske`), not a paragraph.
13
+ const MAX_LANGUAGE = 35;
14
+ // A tag's shape, where the browser can't say (no `Intl.getCanonicalLocales`).
15
+ const SHAPE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/u;
16
+ // The language itself, first: an ISO 639 code, two or three letters. BCP 47
17
+ // also allows five to eight (`english` is well-formed), which nothing uses.
18
+ const PRIMARY = /^[A-Za-z]{2,3}(-|$)/u;
13
19
  // [function]
14
- /** A language the page may send: a short, lowercase tag. */
15
- export function isLanguage(value) {
16
- return typeof value === "string" && LANGUAGE.test(value);
20
+ /**
21
+ * A language tag (BCP 47, as the web, iOS, and Android use) in its canonical
22
+ * form, `zh-hant-tw` as `zh-Hant-TW`, or null if it isn't a well-formed one
23
+ * (`en_US`, `English`, empty). Only its shape is checked, never whether
24
+ * Arcadible uses it, so a language added later is never refused.
25
+ */
26
+ export function languageOf(tag) {
27
+ if (typeof tag !== "string" || tag.length === 0)
28
+ return null;
29
+ if (tag.length > MAX_LANGUAGE || !PRIMARY.test(tag))
30
+ return null;
31
+ if (typeof Intl === "undefined" || !Intl.getCanonicalLocales) {
32
+ return SHAPE.test(tag) ? tag : null;
33
+ }
34
+ try {
35
+ return Intl.getCanonicalLocales(tag)[0] ?? null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
17
40
  }
18
41
  // [function]
19
- /** The state as the game keeps it: only fields it knows, each checked. */
20
- export function stateOf(value) {
21
- if (!isObject(value) || !isLanguage(value["language"]))
22
- return null;
23
- return { language: value["language"] };
42
+ /** Whether a value is a language tag in its canonical form. */
43
+ export function isLanguage(value) {
44
+ return typeof value === "string" && languageOf(value) === value;
24
45
  }
25
46
  // [function]
26
- /** A language tag as a short one: `en-US` as `en`, or null if it isn't one. */
27
- export function languageOf(tag) {
28
- if (typeof tag !== "string")
47
+ /**
48
+ * The state as the game keeps it: only fields it knows, each checked, the
49
+ * language in its canonical form.
50
+ */
51
+ export function stateOf(value) {
52
+ if (!isObject(value))
29
53
  return null;
30
- const short = tag.split("-")[0]?.toLowerCase() ?? "";
31
- return isLanguage(short) ? short : null;
54
+ const language = languageOf(value["language"]);
55
+ return language ? { language } : null;
32
56
  }
33
57
  // [function]
34
- /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
35
- export function shortLanguage(tag) {
58
+ /** A browser's language (`navigator.language`) as the page sends it, or `en`. */
59
+ export function browserLanguage(tag) {
36
60
  return languageOf(tag) ?? "en";
37
61
  }
@@ -1,10 +1,10 @@
1
1
  // The page's side of `system`: Arcadible's state for the player, sent as
2
2
  // the game connects and again whenever it changes. Nothing waits on it.
3
- import { languageOf, Names, shortLanguage } from "./rules.js";
3
+ import { browserLanguage, languageOf, Names, } from "./rules.js";
4
4
  // [function]
5
5
  export function createSystem(initial) {
6
- // only a short tag goes to the game: `en-US` as `en`.
7
- let state = { ...initial, language: shortLanguage(initial.language) };
6
+ // only a well-formed tag goes to the game, in its canonical form.
7
+ let state = { ...initial, language: browserLanguage(initial.language) };
8
8
  let context = null;
9
9
  return {
10
10
  set(next) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.5.0-next.3";
1
+ export declare const VERSION = "1.5.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // The client's version, sent in its hello and shown as `arcadible.version`,
2
2
  // for bug reports. It matches package.json (test/unit/protocol.test.ts).
3
- export const VERSION = "1.5.0-next.3";
3
+ export const VERSION = "1.5.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "author": "Offst, Inc.",
3
- "description": "The Arcadible SDK: Arcadible's loading screen for a game's initialization, and saves that carry over between its versions.",
3
+ "description": "The Arcadible SDK: how your game works with Arcadible.",
4
4
  "devDependencies": {
5
5
  "@playwright/test": "1.63.0",
6
6
  "@types/node": "^25.0.3",
@@ -43,5 +43,5 @@
43
43
  ],
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "1.5.0-next.3"
46
+ "version": "1.5.0"
47
47
  }