@origingame/engine 0.0.0-stage → 0.1.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.
Files changed (158) hide show
  1. package/bin/og.mjs +25 -0
  2. package/dist/web/origin-engine.d.ts +1233 -0
  3. package/dist/web/origin-engine.js +8628 -0
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
  6. package/dist/web-publish/origin-engine-lanes.js +8509 -0
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +1176 -0
  9. package/dist/web-publish/origin-engine.js +8313 -0
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
  12. package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  13. package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
  14. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  15. package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
  16. package/dist/web-publish-2d/origin-engine.js +8313 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  18. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
  19. package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  20. package/knowledge/README.md +31 -0
  21. package/knowledge/knowledge.json +13 -0
  22. package/knowledge/looks/catalogue.json +1871 -0
  23. package/knowledge/package.json +33 -0
  24. package/knowledge/rules/platform.json +329 -0
  25. package/knowledge/scaffold/README.md +27 -0
  26. package/knowledge/scaffold/distribution.json +48 -0
  27. package/knowledge/scaffold/origin-engine.json +19 -0
  28. package/knowledge/scaffold/template/.origin/setup +14 -0
  29. package/knowledge/scaffold/template/LICENSES.md +23 -0
  30. package/knowledge/scaffold/template/README.md +37 -0
  31. package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
  32. package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
  33. package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  34. package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
  35. package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
  36. package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
  37. package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
  38. package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
  39. package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
  40. package/knowledge/scaffold/template/gitattributes +22 -0
  41. package/knowledge/scaffold/template/package.json +18 -0
  42. package/knowledge/scaffold/template/project.ogt +25 -0
  43. package/knowledge/scaffold/template/src/main.ts +246 -0
  44. package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
  45. package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
  46. package/knowledge/scaffold/template/tsconfig.json +17 -0
  47. package/knowledge/scripts/lib.ts +203 -0
  48. package/knowledge/scripts/mount.ts +210 -0
  49. package/knowledge/scripts/pack.ts +24 -0
  50. package/knowledge/scripts/scaffold.ts +72 -0
  51. package/knowledge/scripts/validate.ts +749 -0
  52. package/knowledge/skills/og-ai/SKILL.md +386 -0
  53. package/knowledge/skills/og-audio/SKILL.md +191 -0
  54. package/knowledge/skills/og-build/SKILL.md +330 -0
  55. package/knowledge/skills/og-core/SKILL.md +285 -0
  56. package/knowledge/skills/og-feel/SKILL.md +385 -0
  57. package/knowledge/skills/og-game/SKILL.md +1479 -0
  58. package/knowledge/skills/og-i18n/SKILL.md +220 -0
  59. package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
  60. package/knowledge/skills/og-taste/SKILL.md +537 -0
  61. package/knowledge/skills/og-ui/SKILL.md +444 -0
  62. package/knowledge/wiki/ai.md +627 -0
  63. package/knowledge/wiki/anim.md +1187 -0
  64. package/knowledge/wiki/atmosphere.md +636 -0
  65. package/knowledge/wiki/audio.md +732 -0
  66. package/knowledge/wiki/camera.md +711 -0
  67. package/knowledge/wiki/commands.md +357 -0
  68. package/knowledge/wiki/game.md +220 -0
  69. package/knowledge/wiki/i18n.md +213 -0
  70. package/knowledge/wiki/index.md +1027 -0
  71. package/knowledge/wiki/input.md +1714 -0
  72. package/knowledge/wiki/load.md +1280 -0
  73. package/knowledge/wiki/math.md +166 -0
  74. package/knowledge/wiki/nav.md +1154 -0
  75. package/knowledge/wiki/physics.md +3142 -0
  76. package/knowledge/wiki/platform.md +857 -0
  77. package/knowledge/wiki/probe.md +1692 -0
  78. package/knowledge/wiki/reflect.md +285 -0
  79. package/knowledge/wiki/render.md +3511 -0
  80. package/knowledge/wiki/rng.md +393 -0
  81. package/knowledge/wiki/stream.md +376 -0
  82. package/knowledge/wiki/system.md +197 -0
  83. package/knowledge/wiki/systems.md +43 -0
  84. package/knowledge/wiki/terrain.md +727 -0
  85. package/knowledge/wiki/tilemap.md +422 -0
  86. package/knowledge/wiki/time.md +150 -0
  87. package/knowledge/wiki/transform.md +230 -0
  88. package/knowledge/wiki/tween.md +580 -0
  89. package/knowledge/wiki/ui.md +8000 -0
  90. package/knowledge/wiki/world.md +909 -0
  91. package/package.json +68 -4
  92. package/schema/api.schema.json +29675 -0
  93. package/shell/README.md +22 -0
  94. package/shell/brand/README.md +23 -0
  95. package/shell/brand/icon-192.png +0 -0
  96. package/shell/brand/icon-512.png +0 -0
  97. package/shell/brand/mark-on-paper.svg +6 -0
  98. package/shell/brand/origin-mark.svg +5 -0
  99. package/shell/brand/origin-motion.css +251 -0
  100. package/shell/brand/tokens.css +20 -0
  101. package/shell/brand.ts +132 -0
  102. package/shell/page.ts +715 -0
  103. package/shell/platform.ts +203 -0
  104. package/shell/player.ts +238 -0
  105. package/shell/shell.css +185 -0
  106. package/shell/sw.ts +177 -0
  107. package/shell/transport.ts +515 -0
  108. package/shell/wire.ts +369 -0
  109. package/src/e6.ts +69 -0
  110. package/src/env.d.ts +100 -0
  111. package/src/generated/abi.ts +980 -0
  112. package/src/index.ts +193 -0
  113. package/src/node/audio.ts +86 -0
  114. package/src/node/build.ts +109 -0
  115. package/src/node/device.ts +981 -0
  116. package/src/node/doctor.ts +60 -0
  117. package/src/node/index.ts +102 -0
  118. package/src/node/platform.ts +24 -0
  119. package/src/node/playtest-run.ts +249 -0
  120. package/src/node/png.ts +130 -0
  121. package/src/node/polish.ts +976 -0
  122. package/src/node/run.ts +373 -0
  123. package/src/node/web-build.ts +808 -0
  124. package/src/p2.ts +35 -0
  125. package/src/platform-local.ts +211 -0
  126. package/src/playtest.ts +131 -0
  127. package/src/runtime.ts +1509 -0
  128. package/src/systems/cameras.ts +247 -0
  129. package/src/ui-kit/bind.ts +141 -0
  130. package/src/ui-kit/huds.ts +303 -0
  131. package/src/ui-kit/index.ts +36 -0
  132. package/src/ui-kit/menus.ts +187 -0
  133. package/src/ui-kit/settings.ts +131 -0
  134. package/src/ui-kit/store.ts +222 -0
  135. package/src/ui-kit/story.ts +89 -0
  136. package/src/ui-kit/util.ts +106 -0
  137. package/src/web/a11y.ts +429 -0
  138. package/src/web/audio-worklet.ts +193 -0
  139. package/src/web/audio.ts +307 -0
  140. package/src/web/basis.ts +239 -0
  141. package/src/web/gamepad.ts +123 -0
  142. package/src/web/host.ts +166 -0
  143. package/src/web/input.ts +237 -0
  144. package/src/web/lanes.ts +21 -0
  145. package/src/web/play.ts +20 -0
  146. package/src/web/rpc-ring.ts +110 -0
  147. package/src/web/settle.ts +122 -0
  148. package/src/web/shell.ts +643 -0
  149. package/src/web/text-input.ts +248 -0
  150. package/src/web/worker.ts +1002 -0
  151. package/types/bindings.json +13654 -0
  152. package/types/index.d.ts +20090 -0
  153. package/vendor/basis/LICENSE +201 -0
  154. package/vendor/basis/NOTICE +15 -0
  155. package/vendor/basis/README.md +18 -0
  156. package/vendor/basis/basis_transcoder.js +19 -0
  157. package/vendor/basis/basis_transcoder.wasm +0 -0
  158. package/README.md +0 -3
@@ -0,0 +1,220 @@
1
+ ---
2
+ name: og-i18n
3
+ description: Localizing a game on Origin Engine with og.i18n — string tables per locale as .ogt, messages with arguments, plurals and select (ICU), the host's locale and the player's choice, font chains for Chinese, Japanese, Korean and Arabic, CJK line breaking (kinsoku), shaping and right-to-left layout in og.ui, the cook-time check for missing keys, and how to prove each locale with frames. Use when a game shows text to players in more than one language, adds a language, or shows boxes or broken Arabic.
4
+ ---
5
+
6
+ Every string a player reads comes from a table, never from a literal in a
7
+ system. `og.i18n` picks the table for the locale the host asks for (the
8
+ browser's language, `og run --locale`) or the player chooses, formats the
9
+ message, and hands og.ui the fonts and the direction for it. The API is in
10
+ `reference/i18n.md`; the platform's `localization` skill covers store
11
+ listings and the catalog's languages.
12
+
13
+ ## What works
14
+
15
+ Two games written from this package alone (`examples/lantern-vale`,
16
+ `examples/hollowbell`) ship English and Simplified Chinese from string
17
+ tables: `og polish` found no locale gaps in either, the results template
18
+ showed its localised Best and New best, a name was typed partly through an
19
+ input method, and a skin copied without its CJK `fallback` was refused by
20
+ the cook with the line named. The Kestrel Stair (`examples/kestrel-stair`)
21
+ ships English and Japanese: its Noto Sans JP subset cut by the ui-kit's
22
+ `tools/subset-font.py` needed no other step, and its locales playtest
23
+ finds no English word of a template left in Japanese on eight screens.
24
+
25
+ ## 1. One table per locale
26
+
27
+ ```text
28
+ type strings
29
+ version 1
30
+ ---
31
+ locale "ja"
32
+ fonts "fonts/NotoSansJP-game.otf" # this locale's font chain, first first
33
+ string "menu.play" "プレイ"
34
+ string "hud.coins" "{n, plural, other {コイン#枚}}"
35
+ string "results.time" "タイム {time}"
36
+ ```
37
+
38
+ - One file per locale under `assets/strings/` (`en.ogt`, `zh-CN.ogt`,
39
+ `ja.ogt`, `ko.ogt`, `ar.ogt`); keys are dotted by screen
40
+ (`menu.play`, `pause.resume`, `shop.buy`). Mark the base table `base
41
+ true` (else `en` is the base).
42
+ - Load every table in `start`: `og.i18n.load(world, "strings/ja.ogt")`;
43
+ the active locale is negotiated again after each load (`ja-JP` takes
44
+ `ja`, `zh-TW` takes `zh-Hant` or `zh-TW`, else the base).
45
+ - A key a table lacks falls back to its `fallback` table, then the base, and
46
+ `og cook` warns about it. Treat a cook warning as a bug: `og polish`
47
+ scores missing keys (og-build §5).
48
+
49
+ ## 2. Messages
50
+
51
+ `og.i18n.t(world, "hud.coins", { n: coins })`. The forms:
52
+
53
+ | Form | Example |
54
+ |---|---|
55
+ | an argument | `"Hello, {name}"` |
56
+ | a number, grouped for the locale | `"{score, number} pts"` |
57
+ | plural (CLDR categories `zero one two few many other`, `=N` exact, `#` the number) | `"{n, plural, =0 {No coins} one {# coin} other {# coins}}"` |
58
+ | select | `"{g, select, f {She won} m {He won} other {They won}}"` |
59
+ | a literal brace or quote | `'{'`, `''` |
60
+
61
+ - Big numbers in a locale's own units (K, M, B in English; 万, 亿 in
62
+ Chinese) are table data too: a key for how many digits a unit spans and
63
+ a key per unit, read by one formatter (`templates/archetypes/idle`'s
64
+ `big` in `src/numbers.ts`).
65
+ - Whole sentences per key, never concatenated pieces: word order differs
66
+ (Japanese puts the verb last, Arabic reads right to left).
67
+ - Plurals in every table, even where the language has one form
68
+ (`{n, plural, other {コイン#枚}}`): the argument names stay the same in
69
+ every locale, and `og cook` checks they do.
70
+ - Numbers, times and prices go through arguments, not string building.
71
+ - og.ui's templates have words of their own (Retry, Share, "Best 1,200",
72
+ "New best", Cancel, Settings, Buy, Skip, "1 of 3", a racing HUD's
73
+ "lap", "Best 1:36.233", Pause and Boost): each is a field of
74
+ the template's desc with the English default, so a localised game passes
75
+ its table's: `ResultsDesc.bestLabel`, `newBestLabel`, `shareLabel`,
76
+ `actions`; `ConfirmDesc.confirmLabel`, `cancelLabel`; `SettingsDesc.title`,
77
+ `audioLabel`, `graphicsLabel`, `controlsLabel`, `languageLabel`,
78
+ `masterLabel`, `musicLabel`, `effectsLabel`, `muteLabel`, `qualityLabel`,
79
+ `lowLabel`, `mediumLabel`, `highLabel`, `fpsLabel`, `motionLabel`,
80
+ `contrastLabel`, `sensitivityLabel`, `invertLabel`, `vibrationLabel`,
81
+ `doneLabel`; `ShopDesc.title`, `currency`, `priceLabel`, `buyLabel`,
82
+ `shortLabel` (Buy while the balance is short: "Not enough"),
83
+ `ownedLabel`, `emptyLabel`; `InventoryDesc.title`, `useLabel`,
84
+ `hintLabel` (the detail panel with nothing selected), `countLabel`;
85
+ `LeaderboardDesc.title`, `empty`, `retryLabel` (under `error`, the
86
+ game's own message); `TutorialDesc.skipLabel`, `nextLabel`, `doneLabel`
87
+ (Next on the last step), `stepLabel`; `CutsceneDesc.skipLabel`;
88
+ `LevelSelectDesc.title`; `ResultsDesc.scoreLabel` (over the score:
89
+ "Total time"); `RacingHudDesc.lapLabel` (under the lap), `bestLabel`
90
+ ("Best {best}", the best lap as m:ss.mmm), `pauseLabel` (the pause
91
+ button's name), `boostLabel`, `brakeLabel`, `leftLabel`, `rightLabel`
92
+ (the touch pedals and arrows) and `unit`, and its `splits`' labels
93
+ (`S1`) are the game's. `bestLabel: og.i18n.t(world, "results.best")`
94
+ with `"Best {best}"` / `"最佳 {best}"` in the tables (the template fills
95
+ `{best}`, formatted); `stepLabel` the same with `"{step} of {count}"` /
96
+ `"第 {step} 步,共 {count} 步"`. templates/ui-kit passes every one of
97
+ these words from its five tables (`settings.*`, `shop.*`, `inventory.*`,
98
+ `leaderboard.*`, `tutorial.*`, `cutscene.*`).
99
+ - Emphasis inside a sentence is rich-text markup in the message itself
100
+ (`"Your progress in slot [b]{slot}[/b] will be lost."`, shown with
101
+ `og.ui.richText`): each translation bolds or colours its own words, in
102
+ its own order, instead of the code splitting the sentence into styled
103
+ pieces.
104
+
105
+ ## 3. Fonts for every script
106
+
107
+ - What a screen reader hears is words too, and the same rule holds: a
108
+ group or a picture the kit names for a reader takes its name from the
109
+ desc, with the English default. `LevelSelectDesc.backLabel` (its Back
110
+ button), `starsLabel` (`"{stars} of {max} stars"`, a card's stars and
111
+ the total's: `"{stars} de {max} estrellas"`), `lockedLabel` (`"{name},
112
+ locked"`, a locked card); `SettingsDesc.sectionLabel` (the tab row, the
113
+ select on a phone: "Section") and `languagesLabel` (the language list:
114
+ "Languages"), and `qualityLabel` names the presets as well as heading
115
+ them. Below the templates, `TabsDesc`, `RadioDesc` and `SelectDesc`
116
+ take a `label` (else the reader hears the id) and `IconDesc` and
117
+ `ImageDesc` a `label` (else an icon is its atlas name, `star`; `""` is
118
+ decoration): og-ui §4.
119
+ - Latin and Cyrillic: the skin's font. Every other script: a subset of a
120
+ Noto face for that script holding exactly the characters your tables
121
+ use, named first in that table's `fonts` line.
122
+ - What the engine ships: `fonts/NotoSansSC-Regular-subset.ttf`, in every
123
+ project (Simplified Chinese: GB2312's 6,763 hanzi, kana and Latin-1,
124
+ 2.5 MB), the source for a Chinese subset and the skins' CJK `fallback`.
125
+ The ui-kit's `NotoSansJP-subset.otf`, `NotoSansKR-subset.otf` and its
126
+ Arabic, Hebrew and Devanagari subsets hold only the ui-kit's own strings
127
+ (the Japanese one 246 characters, 74 of them kanji): they are no source
128
+ for a game's text. A Japanese, Korean, Traditional Chinese or Arabic
129
+ game brings the whole Noto face (notofonts, SIL OFL; Noto Sans CJK's
130
+ Regular `.ttc` holds Japanese as face 0, Korean 1, Simplified 2,
131
+ Traditional 3) into a folder outside `assets/` (`tools/fonts/`), so the
132
+ cook never sees it, and records its licence.
133
+ - Make the subset with `templates/ui-kit/tools/subset-font.py`, copied into
134
+ the project's `tools/`: it reads every `string` of `assets/strings/`
135
+ (`--locale ja` for one table, `--extra` for what players or playtests
136
+ type), writes the subset and its import settings (`chars` listing every
137
+ character) and exits 1 naming each character the source lacks. Run it
138
+ again after a string changes, then `og cook`:
139
+
140
+ ```sh
141
+ python3 tools/subset-font.py tools/fonts/NotoSansCJK-Regular.ttc assets/fonts/NotoSansJP-game.otf --face 0 --locale ja
142
+ python3 tools/subset-font.py assets/fonts/NotoSansSC-Regular-subset.ttf assets/fonts/NotoSansSC-game.ttf --locale zh-CN --cjk-only
143
+ ```
144
+
145
+ `--cjk-only` leaves Latin out, so a label mixing scripts takes its Latin
146
+ from the skin's face run by run; with Latin kept, the whole label is
147
+ drawn in the subset (og-ui §7 has the rule).
148
+ - Never take Japanese from the Chinese face: it lacks the Japanese forms of
149
+ common kanji (鐘 鈴 険 続 設 語 楽 効 are not in it) and draws the kanji
150
+ both scripts share in their Chinese shapes. The fallback chain below is a
151
+ safety net for a character a subset missed, not a font for a language:
152
+ a Japanese string that reaches the skin's Chinese `fallback` is drawn in
153
+ Chinese forms, and one no font has is a box with a warning on the first
154
+ frame that draws it.
155
+ - Each font's import settings (`<font>.ogt`) list its characters (`chars`)
156
+ or a charset: `ascii`, `latin`, `kana`, `hangul`, `arabic`. A font
157
+ cooked with the `arabic` charset also carries its joining forms and its
158
+ outlines, which og-ui's shaper needs.
159
+ - og-ui looks a glyph up in the active locale's fonts first, then every
160
+ other loaded table's, then `UiDesc.fallbackFonts` (fonts for scripts no
161
+ table names: a Hebrew or Devanagari face), then the skin's. A box (tofu)
162
+ in a frame means a font lacks the character: add it to the subset and
163
+ to `chars`. The engine's log says so on the tick a character is first
164
+ drawn, once per character, naming it, the font's asset and what its atlas
165
+ holds (`WARN tick 2603 a frame drew "−" U+2212, which font
166
+ skins/neon/ChakraPetch-BoldItalic.ttf lacks (its atlas holds charset
167
+ latin punct)`): the packs cook `latin punct`, which has the hyphen-minus
168
+ `-` but not the minus sign U+2212 nor the arrows U+2190 to U+2193. One
169
+ label is drawn in one font, so the font that covers a
170
+ Hebrew word must also have the digits beside it.
171
+ - Player-typed text (a name, a chat line) is not in any table: cook the
172
+ CJK face with `runtime true` in its import settings and the characters
173
+ outside its atlas are drawn at run time (about 1 to 2 ms each, once per
174
+ character), with a warning the first time. Keep `chars` for everything
175
+ the tables hold, so a frame never waits on them; `charset all` for CJK
176
+ is about 20 MB and never the answer.
177
+ - Complex scripts are shaped from the font file the cook keeps: Arabic
178
+ joins, Devanagari conjuncts and pre-base vowel signs, marks over their
179
+ bases. Cook such a font with the script's charset (`arabic`) or its
180
+ `chars`; check a frame of every shaped string (a conjunct drawn as
181
+ separate letters means the font or its file is missing). Kerning pairs
182
+ apply in every font that has them: nothing to set.
183
+ - Chinese, Japanese and Korean break between characters under kinsoku:
184
+ closing brackets and quotes, 、。,?!, small kana, ー and the iteration
185
+ marks never start a line, opening brackets never end one, and a katakana
186
+ word stays whole (オリジン・ / クエスト, not オリジン・クエス / ト);
187
+ Latin breaks at spaces. Nothing to configure: write the string as it
188
+ reads and check the frame.
189
+
190
+ ## 4. Right to left
191
+
192
+ Arabic, Hebrew, Persian and Urdu locales set the UI's direction to
193
+ right-to-left: og.ui mirrors the whole layout about the surface's centre
194
+ (a corner anchored top-left moves top-right, rows run right to left),
195
+ flips start and end alignment, and orders words by direction while numbers
196
+ and Latin words inside stay left to right (by the Unicode bidirectional
197
+ algorithm, over each whole line of `og.ui.richText`; a label takes the
198
+ direction of its first strong character). Do not mirror the play field or
199
+ the camera. Icons are drawn as they are: give a direction icon (back, next)
200
+ its mirrored twin for right to left, and leave icons of objects alone.
201
+
202
+ ## 5. The player's choice
203
+
204
+ The settings screen's language list calls `og.i18n.setLocale(world, tag)`
205
+ and saves the choice with the other settings (`og.platform.saveSettings`).
206
+ Restore it: `og.platform.loadSettings` on the first tick, and
207
+ `og.i18n.setLocale` with the saved tag when it answers (og-platform-sdk §3);
208
+ a game that only saves comes back in the host's language after a reload. `og.i18n.locale(world)`
209
+ answers the requested and active locale, the direction and the font chain.
210
+
211
+ ## 6. Prove it
212
+
213
+ - A playtest per locale that matters (`templates/ui-kit/tests/locales.og.ts`):
214
+ `locale:<tag>` in the command script switches the locale, then
215
+ `screenshot` each main screen and compare with references; read each
216
+ frame for boxes, clipped words and wrong direction.
217
+ - Check text fits: the longest translation (German, Russian) of each
218
+ button in `dump_ui` stays inside its element.
219
+ - `og cook` prints `warning:` lines for missing or mismatched keys; the
220
+ build is not done while any remain.
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: og-platform-sdk
3
+ description: The OriginGame platform from inside an Origin Engine game — og.platform's store (saves with revisions and conflicts), leaderboards, telemetry, Origin Point, AI-content marks and the settings helpers, as requests answered at a later tick; how they work in the Player, on the stage and under og run; how to test them with dump_platform and --platform-store; and what to ask the platform for when a method is missing. Use when a game saves progress or settings, shows scores, files events or asks for points (the platform's og-platform skill covers the Player protocol around it).
4
+ ---
5
+
6
+ Game code never talks to the platform. It asks the engine, `og.platform.*`,
7
+ by method name and arguments; the host answers: the web shell over the
8
+ Player's protocol 2 (to the Player framing the page, or, when the platform
9
+ opens the published page itself at the top level, to its play origin over
10
+ same-origin HTTP), falling back to the browser's own storage; `og run`
11
+ from a local JSON store. No credential, URL or cookie reaches game code,
12
+ and every call works (or answers `unavailable`) everywhere, so the game
13
+ never branches on where it runs. The API is in `reference/platform.md`;
14
+ the platform's `og-platform` skill is the Player and readiness side.
15
+
16
+ ## What works
17
+
18
+ Hollowbell (`examples/hollowbell`, written from this package alone) keeps
19
+ its run in the store (`og.platform.storeSet` at the revision it read, a
20
+ conflict read again and written back) and proves Continue across a
21
+ relaunch with a `store: "keep"` playtest; Lantern Vale
22
+ (`examples/lantern-vale`) keeps its settings through
23
+ `og.platform.saveSettings`. The relaunch worked the first time it was
24
+ asked for as §4 says. The Kestrel Stair (`examples/kestrel-stair`) keeps
25
+ its whole world at a waystone (`world.snapshot()`, about 78 KB of text, in
26
+ parts under the store's 64 KiB a key) and continues it after a
27
+ `store: "keep"` relaunch, its progress at the revision read with a
28
+ conflict merged, and a won climb on an ascending leaderboard. The store's
29
+ limits are the platform's (64 KiB a key, 64 keys and 1 MiB per player and
30
+ game), so the world goes through `og.platform.saveSlot`: its parts
31
+ (`run.a0`, `run.a1`) in whichever of the slot's two banks of keys
32
+ (`<slot>.a0..`, `<slot>.b0..`) the last save did not use, then the
33
+ manifest `run` at the revision read, so a save cut short leaves the
34
+ previous one whole and two devices saving at once get a conflict, not a
35
+ mix; one request a call. Its title reads `og.platform.loadSlot` with
36
+ `metaOnly` for Continue's place and hour, and Continue reads the parts,
37
+ checked against the manifest's length and checksum.
38
+
39
+ ## 1. Requests, answered later
40
+
41
+ Every call returns a request number at once; the answer arrives at the
42
+ start of a later tick, when `og.platform` sends `og.PlatformAnswered`.
43
+
44
+ ```ts
45
+ // templates/archetypes/runner's Records system.
46
+ if (time.tick === 0) {
47
+ const asked = og.platform.storeGet(world, "best"); // the request, at once
48
+ run.bestRequest[i] = asked.ok ? asked.value : 0;
49
+ return;
50
+ }
51
+ const request = run.bestRequest[i]!;
52
+ if (request === 0) return;
53
+ const answer = og.platform.result(world, request); // null while in flight
54
+ if (answer === null) return;
55
+ run.bestRequest[i] = 0;
56
+ if (answer.ok && typeof answer.data.value === "number") run.best[i] = Math.max(run.best[i]!, answer.data.value);
57
+ ```
58
+
59
+ - Keep the request number in a component or resource (snapshots carry it),
60
+ never in a closure.
61
+ - Answers are host input, like key presses: a replay records them, so the
62
+ same answers give the same world.
63
+ - Every answer carries `time` (UTC ms): the host's clock when it was
64
+ answered. `og.platform.now(world)` is the host's clock at the last
65
+ answer (or when the platform attached) advanced by the ticks since, so
66
+ it is the one wall clock a game may read: the time away is a store
67
+ answer's `time` less the time the save wrote down
68
+ (`templates/archetypes/idle`, `tests/away.og.ts`). `info(world).clock`
69
+ says whose it is: `player`, `browser` (the web host's own, until the
70
+ Player sends its time), `system` (native), `fixed` (`og run --clock`,
71
+ playtests), `none` before the platform attaches (`now` answers 0).
72
+ - An error has a stable `code` (`offline`, `timeout`, `invalid`,
73
+ `unauthorized`, `not_found`, `conflict`, `too_large`, `rate_limited`,
74
+ `unavailable`, `failed`) and `retryable`. Handle `unavailable` as
75
+ "this feature is off here" and keep playing.
76
+
77
+ ## 2. What to use
78
+
79
+ | Need | Call | Notes |
80
+ |---|---|---|
81
+ | Progress, unlocks, a best score | `storeGet(world, key)`, `storeSet(world, { key, value, revision })`; the whole world: `world.snapshot()` text through `saveSlot(world, { slot, text, meta })` (as many keys as it needs, the manifest last, one request) and `loadSlot(world, { slot, metaOnly })`, back with `world.restore` (og-game §17) | one key per independent thing; values ≤ 64 KiB, at most 64 keys and 1 MiB per player and game; pass the revision you read to refuse a stale write (`conflict`, whose `error.revision` is where the key is now: read again and merge), `create: true` to write only a key never written, no revision for "the last write wins" |
82
+ | Volume, controls, language | `loadSettings(world)`, `saveSettings(world, settings)` | the key `og.settings`, last save wins, local fallback when offline |
83
+ | A leaderboard | `leaderboardSubmit(world, { board, score })`, `leaderboardTop`, `leaderboardAround` | the board keeps the player's best; show "around me" as well as the top |
84
+ | Analytics | `telemetry(world, "round.over", { score, coins })` | one-way; a name of 1–64 letters, digits, `.` and `_` (the Player files it as `game.custom.<name>`), at most 16 scalar attributes; never personal data |
85
+ | Origin Point | `point(world, { reason, amount })` | the platform decides what it grants (today nothing under a Player: `{ granted: 0, balance: null }` until an approved point policy; show nothing that depends on it) |
86
+ | AI-generated content shown | `aiMark(world, { kind, label })` | disclosure, not permission |
87
+
88
+ `og.platform.info(world)` says what answers here (`player`, `local` or
89
+ `none`) and the methods it answers (`capabilities`): check it rather than
90
+ trying.
91
+
92
+ ## 3. Rules
93
+
94
+ - Read saves on the first tick and show defaults until they answer;
95
+ never block the title on the platform. On the web the title behind the
96
+ play gate is not tick 1's defaults: the shell takes the first tick, then
97
+ one more tick each time the store answers what that tick asked (the
98
+ settings, a slot's manifest, then its parts), up to 8 ticks, before the
99
+ first frame while the answers come within 300 ms and drawn as they land
100
+ for 5 s after (docs/web-export.md §The shell). So apply an answer the
101
+ tick it arrives (the language, Continue's label), and apply it before
102
+ the title is built that tick: systems run in `GameDesc.systems` order,
103
+ so the system that reads the answers comes before (or is) the one that
104
+ builds the title. A title built by a `Ui` system listed before an
105
+ `Answers` system draws that tick in the defaults even though the answer
106
+ landed, and the frame behind the gate is that tick's: the saved Spanish
107
+ shows only after the tap. The fix is the order: list the system that
108
+ reads every answer first (`systems: [Answers, ..., Ui, Saves]`, writes
109
+ last), or read the answers at the top of the system that builds the
110
+ title. A title that waits for a later tick, or a screen transition that
111
+ only starts after the tap, still shows its defaults behind the gate.
112
+ - The frame behind the gate is a still of the opening title a few ticks
113
+ in, held until the tap, so anything on it that moves into place over
114
+ its first second stands part-way there: the skin's panel entrance and
115
+ its controls' stagger (about half a second), a transition, a fade the
116
+ game plays itself. Build the screen the game opens on settled while it
117
+ is that screen, and let every later screen enter as usual:
118
+ `og.ui.mainMenu("Title", { ..., instant: !g.leftTitle })` with
119
+ `g.leftTitle` set once the shown screen is another (examples/kestrel-stair
120
+ `src/ui.ts`); `og.ui.anchor` and `og.ui.transition` take the same
121
+ `instant`. A game's own intro on the title (a fade from black, a logo)
122
+ starts complete on the first tick, or plays after the player's first
123
+ press.
124
+ - Settings are a pair: a game that calls `saveSettings` also calls
125
+ `loadSettings` on its first tick and, when it answers, applies every
126
+ field it saves, through the same code its settings screen uses. Ask
127
+ for the saved language with `og.i18n.setLocale` even when it matches
128
+ the default (the host's own locale may be another). Saving alone
129
+ passes every test that never relaunches, and every reload comes back
130
+ in the defaults. A `store: "keep"` playtest whose relaunch reads the
131
+ setting back proves it (§4; `templates/ui-kit/tests/settings-saved.og.ts`).
132
+ - Save on meaningful moments (a new best, a level done, settings closed),
133
+ not every tick: writes are rate limited.
134
+ - Never store anything the game can recompute, and never a secret: the
135
+ store belongs to the player, not to the game.
136
+ - The engine exposes no identity (no player id, name or account): a game
137
+ that needs a display name asks for it (§5).
138
+
139
+ The settings read, as the UI kit does it (`followSettings` in
140
+ `templates/ui-kit/src/main.ts`, called by its system every tick):
141
+
142
+ ```ts
143
+ if (time.tick === 0) {
144
+ d.settingsRequest = og.platform.loadSettings(world); // og.settings, answered later
145
+ } else if (d.settingsRequest !== 0) {
146
+ const answer = og.platform.result(world, d.settingsRequest);
147
+ if (answer !== null) {
148
+ d.settingsRequest = 0;
149
+ const saved = answer.ok ? (answer.data as { value?: unknown } | null)?.value : null;
150
+ if (saved && typeof saved === "object") takeSettings(world, d, saved as Partial<GameSettings>, true); // volumes, language, contrast, motion
151
+ }
152
+ }
153
+ ```
154
+
155
+ ## 4. Test it
156
+
157
+ - `dump_platform[:file]` lists every call with its method, arguments, tick,
158
+ state and answer, and what answers them: assert on it in a playtest
159
+ (`templates/archetypes/runner/tests/records.og.ts`; a stale write read
160
+ again and merged: `templates/archetypes/idle/tests/offline.og.ts` and
161
+ `templates/archetypes/roguelike/tests/win.og.ts`).
162
+ - `og run --platform-store save.json` keeps the local store in a file, so
163
+ a second run sees the first's save; without it the store is in memory
164
+ for the run. `og run --clock <UTC ms>` fixes the host clock at tick 0
165
+ (it then moves with the ticks); a playtest runs at 2026-01-01 00:00 UTC
166
+ unless its `clock` says otherwise.
167
+ - A relaunch across processes: a playtest with `store: "keep"` keeps one
168
+ local store (`<out>/.store/platform.json`, a dot name no step's output
169
+ may take, so `dump_platform:platform.json` is safe) for its run and its `then` steps (`{ commands, clock }`,
170
+ each a fresh `og run`), and `check` reads each step's run in
171
+ `run.then[n]` (`templates/archetypes/idle/tests/away.og.ts`: saved at
172
+ `clock`, launched two hours later). A command that relaunches in the
173
+ same process stays for tests that need no second process.
174
+ - In the browser, `packages/engine/tests/web/player.ts` shows the Player
175
+ side: a framed game's first calls wait for the Player's `hello`, then go
176
+ as protocol 2 `call`s (`store.get`, `store.set`, `leaderboard.around`,
177
+ `point.award`, `ai.mark`, as the hello lists them; `docs/web-export.md`
178
+ §Platform services has the mapping).
179
+ - The same file then opens the build as a top-level page with a stand-in
180
+ play origin: the same calls go to `/__player/v2/<game>/<release>/call`
181
+ with the answers unchanged, so a game behaves the same framed or not
182
+ (`docs/web-export.md` §The top-level page). Nothing in game code changes
183
+ between the two, and nothing in it can tell them apart.
184
+
185
+ ## 5. When a method is missing: ask
186
+
187
+ The engine maps what the Player's protocol 2 offers and answers the rest
188
+ `unavailable`. Every ask the engine made (the engine repository's
189
+ `docs/platform-asks.md`: the keyed store with per-key revisions,
190
+ `leaderboard.around`, the best on submit, `point.award`, `ai.mark`,
191
+ capabilities for a verification run, telemetry names) landed on
192
+ 2026-10-19; an older Player answers what its hello lists. One is open:
193
+ the Player's `hello` and answers carrying its `time` (until then the web
194
+ host's answers carry the browser's clock and `info(world).clock` says
195
+ `browser`). When a game
196
+ needs something none of them gives, do not work around it with a server of
197
+ your own or by reaching the page: write the ask (the method, its arguments,
198
+ its answer, why a game needs it) for the platform owners and keep the
199
+ feature behind `info(world)` until it lands.