@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.
- package/bin/og.mjs +25 -0
- package/dist/web/origin-engine.d.ts +1233 -0
- package/dist/web/origin-engine.js +8628 -0
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
- package/dist/web-publish/origin-engine-lanes.js +8509 -0
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +1176 -0
- package/dist/web-publish/origin-engine.js +8313 -0
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
- package/dist/web-publish-2d/origin-engine.js +8313 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/knowledge/README.md +31 -0
- package/knowledge/knowledge.json +13 -0
- package/knowledge/looks/catalogue.json +1871 -0
- package/knowledge/package.json +33 -0
- package/knowledge/rules/platform.json +329 -0
- package/knowledge/scaffold/README.md +27 -0
- package/knowledge/scaffold/distribution.json +48 -0
- package/knowledge/scaffold/origin-engine.json +19 -0
- package/knowledge/scaffold/template/.origin/setup +14 -0
- package/knowledge/scaffold/template/LICENSES.md +23 -0
- package/knowledge/scaffold/template/README.md +37 -0
- package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
- package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
- package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
- package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
- package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
- package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
- package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
- package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
- package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
- package/knowledge/scaffold/template/gitattributes +22 -0
- package/knowledge/scaffold/template/package.json +18 -0
- package/knowledge/scaffold/template/project.ogt +25 -0
- package/knowledge/scaffold/template/src/main.ts +246 -0
- package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
- package/knowledge/scaffold/template/tsconfig.json +17 -0
- package/knowledge/scripts/lib.ts +203 -0
- package/knowledge/scripts/mount.ts +210 -0
- package/knowledge/scripts/pack.ts +24 -0
- package/knowledge/scripts/scaffold.ts +72 -0
- package/knowledge/scripts/validate.ts +749 -0
- package/knowledge/skills/og-ai/SKILL.md +386 -0
- package/knowledge/skills/og-audio/SKILL.md +191 -0
- package/knowledge/skills/og-build/SKILL.md +330 -0
- package/knowledge/skills/og-core/SKILL.md +285 -0
- package/knowledge/skills/og-feel/SKILL.md +385 -0
- package/knowledge/skills/og-game/SKILL.md +1479 -0
- package/knowledge/skills/og-i18n/SKILL.md +220 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
- package/knowledge/skills/og-taste/SKILL.md +537 -0
- package/knowledge/skills/og-ui/SKILL.md +444 -0
- package/knowledge/wiki/ai.md +627 -0
- package/knowledge/wiki/anim.md +1187 -0
- package/knowledge/wiki/atmosphere.md +636 -0
- package/knowledge/wiki/audio.md +732 -0
- package/knowledge/wiki/camera.md +711 -0
- package/knowledge/wiki/commands.md +357 -0
- package/knowledge/wiki/game.md +220 -0
- package/knowledge/wiki/i18n.md +213 -0
- package/knowledge/wiki/index.md +1027 -0
- package/knowledge/wiki/input.md +1714 -0
- package/knowledge/wiki/load.md +1280 -0
- package/knowledge/wiki/math.md +166 -0
- package/knowledge/wiki/nav.md +1154 -0
- package/knowledge/wiki/physics.md +3142 -0
- package/knowledge/wiki/platform.md +857 -0
- package/knowledge/wiki/probe.md +1692 -0
- package/knowledge/wiki/reflect.md +285 -0
- package/knowledge/wiki/render.md +3511 -0
- package/knowledge/wiki/rng.md +393 -0
- package/knowledge/wiki/stream.md +376 -0
- package/knowledge/wiki/system.md +197 -0
- package/knowledge/wiki/systems.md +43 -0
- package/knowledge/wiki/terrain.md +727 -0
- package/knowledge/wiki/tilemap.md +422 -0
- package/knowledge/wiki/time.md +150 -0
- package/knowledge/wiki/transform.md +230 -0
- package/knowledge/wiki/tween.md +580 -0
- package/knowledge/wiki/ui.md +8000 -0
- package/knowledge/wiki/world.md +909 -0
- package/package.json +68 -4
- package/schema/api.schema.json +29675 -0
- package/shell/README.md +22 -0
- package/shell/brand/README.md +23 -0
- package/shell/brand/icon-192.png +0 -0
- package/shell/brand/icon-512.png +0 -0
- package/shell/brand/mark-on-paper.svg +6 -0
- package/shell/brand/origin-mark.svg +5 -0
- package/shell/brand/origin-motion.css +251 -0
- package/shell/brand/tokens.css +20 -0
- package/shell/brand.ts +132 -0
- package/shell/page.ts +715 -0
- package/shell/platform.ts +203 -0
- package/shell/player.ts +238 -0
- package/shell/shell.css +185 -0
- package/shell/sw.ts +177 -0
- package/shell/transport.ts +515 -0
- package/shell/wire.ts +369 -0
- package/src/e6.ts +69 -0
- package/src/env.d.ts +100 -0
- package/src/generated/abi.ts +980 -0
- package/src/index.ts +193 -0
- package/src/node/audio.ts +86 -0
- package/src/node/build.ts +109 -0
- package/src/node/device.ts +981 -0
- package/src/node/doctor.ts +60 -0
- package/src/node/index.ts +102 -0
- package/src/node/platform.ts +24 -0
- package/src/node/playtest-run.ts +249 -0
- package/src/node/png.ts +130 -0
- package/src/node/polish.ts +976 -0
- package/src/node/run.ts +373 -0
- package/src/node/web-build.ts +808 -0
- package/src/p2.ts +35 -0
- package/src/platform-local.ts +211 -0
- package/src/playtest.ts +131 -0
- package/src/runtime.ts +1509 -0
- package/src/systems/cameras.ts +247 -0
- package/src/ui-kit/bind.ts +141 -0
- package/src/ui-kit/huds.ts +303 -0
- package/src/ui-kit/index.ts +36 -0
- package/src/ui-kit/menus.ts +187 -0
- package/src/ui-kit/settings.ts +131 -0
- package/src/ui-kit/store.ts +222 -0
- package/src/ui-kit/story.ts +89 -0
- package/src/ui-kit/util.ts +106 -0
- package/src/web/a11y.ts +429 -0
- package/src/web/audio-worklet.ts +193 -0
- package/src/web/audio.ts +307 -0
- package/src/web/basis.ts +239 -0
- package/src/web/gamepad.ts +123 -0
- package/src/web/host.ts +166 -0
- package/src/web/input.ts +237 -0
- package/src/web/lanes.ts +21 -0
- package/src/web/play.ts +20 -0
- package/src/web/rpc-ring.ts +110 -0
- package/src/web/settle.ts +122 -0
- package/src/web/shell.ts +643 -0
- package/src/web/text-input.ts +248 -0
- package/src/web/worker.ts +1002 -0
- package/types/bindings.json +13654 -0
- package/types/index.d.ts +20090 -0
- package/vendor/basis/LICENSE +201 -0
- package/vendor/basis/NOTICE +15 -0
- package/vendor/basis/README.md +18 -0
- package/vendor/basis/basis_transcoder.js +19 -0
- package/vendor/basis/basis_transcoder.wasm +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: og-build
|
|
3
|
+
description: The Origin Engine build loop — og cook, og run with a scripted command channel (inputs, captures, dumps, get and set), screenshots and world dumps read as evidence, og dev hot reload, og test playtests, og polish's taste and feel scores (the one list of what it measures and where each finding is fixed), og build for publishing, og run --device for the first frame and frame times on a real browser or phone, and big games: streaming groups and what they may hold, .vox models, compressed animation, chunked music, the lanes and cross-origin isolation. Use whenever a change to a project with a project.ogt must be run, observed, scored or shipped (not a Godot project.godot).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The `og` tool is the whole loop: it cooks assets, runs the game headless on
|
|
7
|
+
the native host with a command script, serves the probe, reloads on save,
|
|
8
|
+
runs playtests, scores them and builds the publishable bundle. Nothing here
|
|
9
|
+
needs a browser or a Rust toolchain. The command channel is
|
|
10
|
+
`reference/commands.md`, the probe `reference/probe.md`.
|
|
11
|
+
|
|
12
|
+
## What works
|
|
13
|
+
|
|
14
|
+
The loop held for two whole games written from this package alone
|
|
15
|
+
(`examples/lantern-vale`, `examples/hollowbell`): `og cook` took 47 and 69
|
|
16
|
+
assets cold in 8.2 s and 3.6 s and under 100 ms after a change, without a
|
|
17
|
+
warning, and every refusal named the line and the fix (a copied skin
|
|
18
|
+
without its CJK `fallback` named the line and og-taste §1;
|
|
19
|
+
`PostEffects.vignette is true or false` named the line). Scripted runs gave
|
|
20
|
+
the same frames every time, `store: "keep"` relaunches and two-process
|
|
21
|
+
replays worked the first time they were asked for, and `og polish` scored
|
|
22
|
+
taste 99 for both and feel 90 and 92, naming the section that fixes each
|
|
23
|
+
finding. The third, The Kestrel Stair (`examples/kestrel-stair`, 3D
|
|
24
|
+
third-person), cooked 62 assets cold in 8.5 s (110 outputs, 9.9 MB) and
|
|
25
|
+
about 0.1 s after a change, without a warning, its refusals naming the
|
|
26
|
+
line; two processes of a full session (chases, bites, strikes, a rest, the
|
|
27
|
+
gate, the bell) ended on one world hash, and `og polish` found every
|
|
28
|
+
input answered within a tick and every damage moment with hit stop, shake,
|
|
29
|
+
a number and a sound (taste 98, feel 79).
|
|
30
|
+
|
|
31
|
+
## 1. The loop
|
|
32
|
+
|
|
33
|
+
1. **Cook** after any change under `assets/`: `og cook`. It is incremental
|
|
34
|
+
(only changed sources re-cook) and writes `cooked/`, the only thing the
|
|
35
|
+
runtime reads. A load that names an uncooked path, or a blob whose cook
|
|
36
|
+
stamp does not match the manifest, is refused with the path; cook again.
|
|
37
|
+
2. **Run with a script**: `og run . --headless --commands "<script>"`. The
|
|
38
|
+
script is `t:action[:args];...` with `t` in simulation seconds from the
|
|
39
|
+
start of the run, so the same script always lands on the same ticks:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
og run . --headless --seed 1 --commands "0.5:touch_move_to:Start;0.6:touch_start;0.7:touch_end;2.0:screenshot;2.2:dump_world;2.5:quit"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Core actions: `screenshot[:path][:full|:N]`, `listen[:path][:seconds]`,
|
|
46
|
+
`touch_move_to:<ui id>`, `move_to_text:[=]text[@N]`, `touch_move:x,y`,
|
|
47
|
+
`touch_start`, `touch_end`, `touch:<id>:<down|move|up>:<x,y | ui id>`
|
|
48
|
+
(a second finger), `gamepad:<a+b+...|->[@pad][:lx,ly][:lt,rt]`, `scroll:dx,dy`,
|
|
49
|
+
`key_down:<Key>`, `key_up:<Key>`, `type:<text>`, `compose:<text>` and
|
|
50
|
+
`commit:<text>` (an input method, og-ui §7),
|
|
51
|
+
`teleport:<entity>:x,y,z`, `frame_entity:<entity>`, `dump_world[:path]`,
|
|
52
|
+
`dump_entity:<entity>[:path]`, `dump_ui[:path]`,
|
|
53
|
+
`get:<Resource>[.<field>]` or `get:<entity>[.<Component>[.<field>]]`
|
|
54
|
+
(logs the value as game code sees it, og.Time whole and a light og.DayNight
|
|
55
|
+
drives as it is drawn, and writes `get-<tick>.json`, then
|
|
56
|
+
`get-<tick>-2.json`, ... for more on one tick; a world dump lists
|
|
57
|
+
resources by name only, so `get:` is how to read one),
|
|
58
|
+
`set:<entity>.<Component>.<field>=<value>` or
|
|
59
|
+
`set:<Resource>.<field>=<value>` (`set:og.Camera.trauma=0.5`,
|
|
60
|
+
`set:Game.coins=10`, `set:og.Stylise.ink=false`: a number, `true` or
|
|
61
|
+
`false`, or `x,y,z`; a value outside the field's `min`/`max` is refused
|
|
62
|
+
naming the range, never clamped, while game code's own writes are stored
|
|
63
|
+
as given: widen the range in the field's options to try one), `seed:<n>`, `pause`, `resume`, `advance:<ticks>`,
|
|
64
|
+
`quit`, and `help` for the list. Modules add their own: `dump_ai` and
|
|
65
|
+
`dump_nav` (og-ai §5), `dump_platform` (og-platform-sdk §4), `locale:<tag>`
|
|
66
|
+
(og-i18n §6), `dump_stream` and `terrain_stream:<stream|resident>[:<MiB>]`
|
|
67
|
+
(og-game §6 and §11). Unknown actions warn and the script continues; read the
|
|
68
|
+
warnings.
|
|
69
|
+
3. **Read what it wrote.** Screenshots are PNG files, dumps are JSON files
|
|
70
|
+
(`<action>-<tick>.png|json` unless the step names a path), written
|
|
71
|
+
under `og-out/` in the project (`--out <dir>` chooses); `og run`
|
|
72
|
+
prints each path. Open the frame and describe what it shows; read
|
|
73
|
+
`dump_world` for the entity count, the archetype tables and each system's
|
|
74
|
+
matched count. A system that matched 0 entities is usually a missing
|
|
75
|
+
component on the spawn, not a logic bug.
|
|
76
|
+
|
|
77
|
+
Where there is no renderer (a sandbox with no GPU: `og run --json` says
|
|
78
|
+
`"tier": "none"`, `og doctor` says the renderer is `none`), the run is
|
|
79
|
+
the same run: the same ticks, dumps and world hash. A `screenshot`
|
|
80
|
+
writes no PNG and logs "no renderer, frame unchecked" with the file it
|
|
81
|
+
would have written, not as a warning; `og run --json` lists those files in
|
|
82
|
+
`unchecked`, and the probe's `capture` answers null. Read the dumps
|
|
83
|
+
there, and look at the frames where there is a renderer (§4).
|
|
84
|
+
4. **Fix and repeat** with the same script and seed. Keep the script in the
|
|
85
|
+
change so the next run is the same run.
|
|
86
|
+
|
|
87
|
+
A game's own actions (`GameDesc.commands`, og-game §3) join the list; give
|
|
88
|
+
each its argument form and one sentence in `GameDesc.commandHelp`
|
|
89
|
+
(`{ spawn_wave: { usage: "<count>", doc: "Spawns a wave now." } }`)
|
|
90
|
+
so `help` explains it (it lists `spawn_wave:<count>`: the name, then the
|
|
91
|
+
usage). An entry naming no action is logged when the game
|
|
92
|
+
starts.
|
|
93
|
+
|
|
94
|
+
## 2. Live sessions: the probe and hot reload
|
|
95
|
+
|
|
96
|
+
- `og run .` without `--headless` keeps running and serves the probe over a
|
|
97
|
+
loopback WebSocket; `og probe <method> [json]` calls it (`og probe hello`,
|
|
98
|
+
`og probe state`, `og probe dump '{"what":"world"}'`,
|
|
99
|
+
`og probe capture '{"width":640,"height":360}'`, `og probe patch …`). A
|
|
100
|
+
refusal is `false` or `null`, never an error; an error means the call was
|
|
101
|
+
malformed.
|
|
102
|
+
- `og dev .` watches `src/` and `assets/`: it rebuilds the bundle, re-cooks
|
|
103
|
+
changed assets and reloads the running game through the probe, keeping the
|
|
104
|
+
world state (components migrate field by field by name). The budget is
|
|
105
|
+
300 ms from save to the first new frame; `og dev` reports each reload's
|
|
106
|
+
latency and any dropped components or fields. A reload that fails keeps
|
|
107
|
+
the old bundle running and prints why.
|
|
108
|
+
|
|
109
|
+
## 3. Tests
|
|
110
|
+
|
|
111
|
+
`og test .` runs the project's bun tests and every `tests/*.og.ts` playtest
|
|
112
|
+
script through `og run --headless --commands`. A playtest asserts on what the
|
|
113
|
+
game exposes (dumps, `state()` facts, entity fields, `get` answers), never on
|
|
114
|
+
how the code is written. Each playtest writes into its own
|
|
115
|
+
`og-out/test/<name>/`, emptied when it starts, and its `check` reads a
|
|
116
|
+
step's file by the name the step gave (`run.json("shop.json")`,
|
|
117
|
+
`run.png("title.png")`); give each step's file its own name. A test that ran no checks fails. Run it before
|
|
118
|
+
claiming a change works.
|
|
119
|
+
|
|
120
|
+
Put every check that reads a frame (`run.png`, a decoded screenshot, a
|
|
121
|
+
comparison with `references/`) inside `t.frame(files, () => ...)`:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
check(run, t) {
|
|
125
|
+
t.equal(run.json<Game>("shop.json").coins, 10, "the coin was banked");
|
|
126
|
+
t.frame(["title.png", "shop.png"], () => checkFrames(run, t, ["title.png", "shop.png"], ""));
|
|
127
|
+
},
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Without a renderer (`run.rendered` false, `run.tier` `"none"`) the scope
|
|
131
|
+
does not run: `og test` prints `~ <file>: no renderer, frame unchecked` for
|
|
132
|
+
each screenshot, counts them in its summary (`og test: 7 suite(s) passed;
|
|
133
|
+
9 frame(s) unchecked`) and passes on every other check, which runs as it
|
|
134
|
+
would with a renderer. A check that reads a frame outside `t.frame` stops
|
|
135
|
+
there (the report says the checks after it did not run), so keep frames in
|
|
136
|
+
the scope. Unchecked frames never change `og test`'s exit status; they are
|
|
137
|
+
verified where there is a renderer: the author's machine, the
|
|
138
|
+
browser-check sandbox (the published page, §6) or an executor with a GPU. A playtest runs with the host clock at
|
|
139
|
+
2026-01-01 00:00 UTC (`clock` to change it) and a fresh store; `store:
|
|
140
|
+
"keep"` with `then: [{ commands, clock }]` runs further processes over the
|
|
141
|
+
same store, for a relaunch (og-platform-sdk §4).
|
|
142
|
+
|
|
143
|
+
## 4. Evidence discipline
|
|
144
|
+
|
|
145
|
+
- Every change that alters rendering, animation, physics or the tick carries
|
|
146
|
+
a scripted `og run --commands` run whose frame was opened, read and
|
|
147
|
+
described in the change: what is on screen, where, and how that differs
|
|
148
|
+
from before. A frame nobody looked at is not evidence. Where there is no
|
|
149
|
+
renderer the frame is unchecked, not seen: say so, and look at it where
|
|
150
|
+
there is one (§3) before calling a visible change done.
|
|
151
|
+
- Name the exact command, seed and script with the frame and the dump, so
|
|
152
|
+
anyone can rerun it and get the same bytes.
|
|
153
|
+
- Determinism is checkable: two runs with the same seed and script give the
|
|
154
|
+
same world hash in `dump_world`. When they do not, find the wall-clock
|
|
155
|
+
read or the unseeded randomness before anything else.
|
|
156
|
+
- Report what ran and what did not. A build that compiled is not a game
|
|
157
|
+
that plays.
|
|
158
|
+
|
|
159
|
+
## 5. Polish, measured: `og polish`
|
|
160
|
+
|
|
161
|
+
This is the one list of what `og polish` measures; og-taste §10 and
|
|
162
|
+
og-feel §13 are the checklists it scores, and every other skill points
|
|
163
|
+
here. `og polish .` plays every playtest headless, samples every tick
|
|
164
|
+
(`dump_pulse`: time scale and hit stop, camera trauma, punch and kick,
|
|
165
|
+
floating texts, afterimages, tweens, particle systems, voices by bus, the
|
|
166
|
+
named entities' positions and flipbook frames, step and render time), takes
|
|
167
|
+
`dump_ui` beside every `screenshot` and captures the audio. It writes
|
|
168
|
+
`og-out/polish/report.md`: a **taste** and a **feel** score (0 to 100 each,
|
|
169
|
+
the mean of their rows; a row is n/a when no playtest exercises it), the
|
|
170
|
+
frames, and for each finding the skill section that fixes it. It refuses a
|
|
171
|
+
project without playtests: write them first (§3), with a `screenshot` on
|
|
172
|
+
every screen that matters.
|
|
173
|
+
|
|
174
|
+
| Axis | Measure | From | Passes at | Fixed in |
|
|
175
|
+
|---|---|---|---|---|
|
|
176
|
+
| taste | Contrast | each text element's rectangle in a shot: the ground (its median) against its ink, read over the text's own glyphs (the pixels at least half-way to the strongest ink; the farthest twentieth of them, the glyph cores) | 4.5:1 | og-taste §3 |
|
|
177
|
+
| taste | Text size | the tallest run of ink rows in each text element, CSS px | 9 px, a 13 px face's capitals | og-taste §4, §10 |
|
|
178
|
+
| taste | HUD occupancy | the union of leaf elements on gameplay shots, touch controls (joystick, d-pad, action buttons) left out; a shot with a dialog or loading layer, a conversation, a list over a quarter of the surface or three texts in the central third is a screen, not gameplay | at most 15 % of the surface and 5 % of the central third | og-taste §2 |
|
|
179
|
+
| taste | Screen transitions | a change of three or more elements with UI motion or a tween in the 20 ticks around it | every change | og-taste §5 |
|
|
180
|
+
| taste | UI sounds | a press that changed the UI and started a voice on the ui bus within 8 ticks | every press | og-taste §6, og-audio §3 |
|
|
181
|
+
| taste | Locale keys | `og cook`'s warnings | none | og-i18n §1 |
|
|
182
|
+
| feel | Input latency | ticks from the tick an input lands to the first thing seen or heard to change (a named entity accelerating or changing clip, the UI, a sound, a tween, a floating text, shake, hit stop, a particle burst); a press answered on its release counts from the release | 0–1 ticks | og-feel §1 |
|
|
183
|
+
| feel | Hit feedback | each damage moment (a fact named `hp`, `health`, `lives`… falling, or `hits`, `damage`, `kills`… rising): hit stop, shake, floating text and a sound within 8 ticks | three of the four | og-feel §3, §4, §7, og-audio §1 |
|
|
184
|
+
| feel | Idle frames | ticks after the first ten where nothing moves or animates, no UI moves, no sound starts or plays, no tween, text or particle runs | at most 10 % | og-feel §9, og-taste §5 |
|
|
185
|
+
| feel | Sound on input | inputs answered by a sound within 6 ticks | every action | og-audio §1 |
|
|
186
|
+
| feel | Frame time | step + render, p95, on the tier it ran on (`metal` on a Mac, `vulkan` on a GPU); the probe's own screenshots (render, read back, PNG) are not the game's step and are left out. Without a renderer the row says `n/a (no renderer)`, as do contrast, text size and HUD occupancy, which read frames | 8.3 ms (half the frame) | og-game §14 |
|
|
187
|
+
|
|
188
|
+
The report also lists clipped samples in the mix (og-audio §4). Contrast and
|
|
189
|
+
text size read text only: icons and images (their "text" is a picture's
|
|
190
|
+
name), disabled controls and what they hold (WCAG exempts them), the
|
|
191
|
+
slivers a scroll container cuts, what a dialog's or a tutorial's scrim
|
|
192
|
+
dims on purpose and what is mid-transition in the shot (an element whose
|
|
193
|
+
bounds differ on the ticks before and after it: a toast sliding out over
|
|
194
|
+
a dialog, a panel popping in) are left out, and each element is measured
|
|
195
|
+
at the pixels of the surface `dump_ui` reports, so a phone preview
|
|
196
|
+
(390×844 at 3×) is read at its own size. The ink is measured over the
|
|
197
|
+
glyphs, not the rectangle, so a short word in a wide label (a three-letter
|
|
198
|
+
name on a 356 px leaderboard row) reads what its letters show, as a full
|
|
199
|
+
line does, and not its antialiased edges diluted by the empty rest of the
|
|
200
|
+
row. The ink is what the pixels show,
|
|
201
|
+
so it depends on the glyphs' coverage: thin strokes cover few pixels
|
|
202
|
+
whole, and a regular-weight CJK face at body sizes measures lower than
|
|
203
|
+
Latin in the same colours (a 15 px "设置" 3.1:1 where "Settings" reads
|
|
204
|
+
4.6:1 on one button), as it reads lower to the eye. Darken the ink in the
|
|
205
|
+
skin's palette, give the CJK face a heavier weight or the label a larger
|
|
206
|
+
step; the measure is not wrong there. Facts are the game's `facts`
|
|
207
|
+
(og-game §3): expose `hp`, `hits` or `kills` there or the hit measures stay
|
|
208
|
+
n/a. A baseline line says how often things change with no input at all;
|
|
209
|
+
when it is high (a runner that always moves), read latency together with
|
|
210
|
+
the frames.
|
|
211
|
+
|
|
212
|
+
The numbers cannot judge a style. Read the frames before changing anything,
|
|
213
|
+
fix the cause in the skin, the template or the system, never per call, and
|
|
214
|
+
run it again after the fix: the report is the evidence.
|
|
215
|
+
|
|
216
|
+
## 6. Shipping
|
|
217
|
+
|
|
218
|
+
`og build .` bundles `src/` to `dist/game.js`, cooks, and copies the engine
|
|
219
|
+
runtime and wasm next to it. `og build . --publish` produces the published
|
|
220
|
+
form: the probe keeps only its player-facing members (hello, tick, state,
|
|
221
|
+
pick, entity, input, capture) and the command channel is gone.
|
|
222
|
+
What ships is still checked by script, through what it keeps: real input
|
|
223
|
+
on the published page and the probe's `tick`, `state` and `capture`. A
|
|
224
|
+
playtest written in input steps (`key_down`, `type`, `touch_move_to` an
|
|
225
|
+
id, `move_to_text`, `touch_start`) and `screenshot` steps runs there as it
|
|
226
|
+
runs under `og test`, each frame compared with the same references; a
|
|
227
|
+
game command (warp, give) has no channel there and is refused, so keep
|
|
228
|
+
the steps a published check needs free of them. The engine runs this on
|
|
229
|
+
the published build of its own games (its `tests/web/published.ts`: the
|
|
230
|
+
boot, the title, a press, a typed name and a frame against the metal
|
|
231
|
+
reference, with a clean console).
|
|
232
|
+
`og run --device chrome .` plays that publish build in a real browser
|
|
233
|
+
(`android[:serial]` over adb, `iphone[:udid]` over safaridriver), passes
|
|
234
|
+
the play gate and writes `og-out/device/report.md`: the first frame against
|
|
235
|
+
its budget (2.0 s, 1.5 s on an iPhone), the frame-time percentiles on the
|
|
236
|
+
tier the device chose, the device itself, and a fix per finding (og-game
|
|
237
|
+
§6 for a slow first frame, og-game §14 for missed frames). Run it on the
|
|
238
|
+
phone you ship for: og polish's native numbers are not a phone's. `og doctor`
|
|
239
|
+
names each missing tool (bun, the native addon, the wasm artifact) with its
|
|
240
|
+
fix, and says whether this machine has a renderer (none is not a problem:
|
|
241
|
+
the playtests' frames are then unchecked, §3).
|
|
242
|
+
|
|
243
|
+
## 7. Big games: what the cook packs and the page fetches
|
|
244
|
+
|
|
245
|
+
What `og cook` takes is decided by the file: `.glb`/`.gltf` and MagicaVoxel
|
|
246
|
+
`.vox` (meshed into a glTF: flat-shaded quads, palette colours, `model {
|
|
247
|
+
voxScale voxOrigin }` for the size and origin, frames as clips; og-game §5)
|
|
248
|
+
are models; `.ogt` files by their `type` (scene,
|
|
249
|
+
look, environment, texture-array, flipbook, particles, terrain, shader…);
|
|
250
|
+
`.cube` LUTs, `.hex` and `.gpl` palettes, images, WAVs and fonts by
|
|
251
|
+
extension. A look's built-in `@canvas/…` and `@palettes/…` assets are added
|
|
252
|
+
to the bundle only when a look names them; a game's own `assets/@canvas/`
|
|
253
|
+
is refused.
|
|
254
|
+
|
|
255
|
+
**Streaming groups** (og-game §6) are cooked into one pack each,
|
|
256
|
+
`cooked/stream/<name>.ogb`, and `og build --publish` ships each as its own
|
|
257
|
+
file the page fetches when the group loads. A group may hold glTF models
|
|
258
|
+
without skins or morph targets, textures, texture arrays, sounds,
|
|
259
|
+
environment maps, palettes, looks and LUTs:
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
stream "night-market" {
|
|
263
|
+
asset "models/stalls.glb"
|
|
264
|
+
asset "env/night.ogt" # an environment map
|
|
265
|
+
asset "tiles/market.ogt" # a texture array
|
|
266
|
+
asset "palettes/neon.hex"
|
|
267
|
+
asset "looks/night-market.ogt" # applied once resident
|
|
268
|
+
asset "grade/night.cube"
|
|
269
|
+
volume "market-gate"
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A sound cooked in chunks may join a group too: its first chunk goes in
|
|
274
|
+
the pack and every other chunk stays a file of its own, read ahead of the
|
|
275
|
+
playheads once the group is resident (og-audio §5). The cook refuses any
|
|
276
|
+
other kind (a skinned model, a scene) in a group, naming the scene and
|
|
277
|
+
the asset. Prove a group in a script:
|
|
278
|
+
move the camera into its trigger (`teleport`, or the game's own command),
|
|
279
|
+
`advance` a few ticks, then `dump_stream` lists each group's state,
|
|
280
|
+
memory and the assets kept past an unload (`retained`), and
|
|
281
|
+
`get:` of a field the game copies from `og.stream.budget` shows `used` and
|
|
282
|
+
`kept` against `budget`. A run with the same script must end on the same
|
|
283
|
+
world hash streamed or not: nothing a tick reads depends on what is
|
|
284
|
+
resident.
|
|
285
|
+
|
|
286
|
+
**Size knobs** in a model's or a sound's import settings (`<file>.ogt`,
|
|
287
|
+
`type import`):
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
animation {
|
|
291
|
+
compress true # quantised clips, keyframes the curve rebuilds dropped
|
|
292
|
+
tolerance 0.0001 # the most a sampled value may move (og-game §9)
|
|
293
|
+
}
|
|
294
|
+
sound {
|
|
295
|
+
stream true # cooked in chunks (the default past 1 MiB of QOA, about 27 s of stereo)
|
|
296
|
+
chunk 4 # seconds per chunk (og-audio §5)
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**What a player downloads** is what `og build --publish` prints under its
|
|
301
|
+
size table, brotli `-q 11` per file as the size gate measures the engine:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
download (brotli-11, what a player fetches; a WebGPU and a WebGL2 page fetch the same files):
|
|
305
|
+
boot, lanes page (isolated) 92 files 4323854 B (engine 2976.8 kB, cooked 1044.3 kB, shell 85.5 kB, basis 217.3 kB)
|
|
306
|
+
boot, single-threaded page 91 files 4284988 B (engine 2937.9 kB, cooked 1044.3 kB, shell 85.5 kB, basis 217.3 kB)
|
|
307
|
+
later: 20 sound chunks 2427050 B (ahead of their playheads); the audio worklet 875 B (first gesture); the service worker and icons 32634 B
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The boot is everything fetched before the first frame: one engine module
|
|
311
|
+
(the lanes module on a cross-origin isolated page, else the
|
|
312
|
+
single-threaded one), the shell, the Basis transcoder and the cooked first
|
|
313
|
+
group (everything no streaming group holds). A sound cooked in chunks
|
|
314
|
+
costs the boot its first chunk only (about `chunk` seconds of QOA): the
|
|
315
|
+
rest are fetched ahead of its playheads, and none before it plays; in a
|
|
316
|
+
streaming group it costs the boot nothing (its first chunk comes in the
|
|
317
|
+
group's pack). The
|
|
318
|
+
table's `total` is every file of the directory (both engine modules, the
|
|
319
|
+
licences, the icons), never a download. Compare the boot line before and
|
|
320
|
+
after a change; `--json` carries the same as `downloads`.
|
|
321
|
+
|
|
322
|
+
**The lanes** spread physics, skinning, culling and extraction over the
|
|
323
|
+
cores with results identical to one thread (og-game §14). Natively
|
|
324
|
+
`OG_LANES=off` (or `1`) runs serial and `OG_LANES=<n>` uses n threads; a
|
|
325
|
+
world hash must not change between them, which is a check worth a
|
|
326
|
+
playtest of its own on a physics-heavy game. On the web the lanes need a
|
|
327
|
+
cross-origin isolated page: `project.ogt`'s `publish { isolation
|
|
328
|
+
required | preferred | off }` (`preferred` by default: lanes where the
|
|
329
|
+
host serves COOP/COEP, one thread otherwise, the same game either way).
|
|
330
|
+
The lanes build caps memory at 2 GiB. `dump_lanes` reports what ran where.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: og-core
|
|
3
|
+
description: The rules every Origin Engine game obeys — TypeScript only, a fixed deterministic tick, Results instead of throws, typed views per archetype, no wall clock, and reflection as the one schema. Read before writing or reviewing any code in a project with a project.ogt.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Origin Engine is a Rust engine with a TypeScript API, generated from one IDL
|
|
7
|
+
(`crates/og-api/api.ogt` in the engine repository). The same game bundle runs
|
|
8
|
+
in the browser (wasm in a Worker) and headless on the native host under
|
|
9
|
+
`og run`. These rules are what makes that true; breaking one produces a game
|
|
10
|
+
that works on one host, one seed or one machine and not the next.
|
|
11
|
+
|
|
12
|
+
## What works
|
|
13
|
+
|
|
14
|
+
Two games written from this package alone (`examples/lantern-vale`, a 3D
|
|
15
|
+
adventure, and `examples/hollowbell`, a 2D platformer) hold these rules
|
|
16
|
+
for a whole game, not a toy: each ends a full session run in two processes
|
|
17
|
+
on one world hash, a player's name and a save live in `str` fields as
|
|
18
|
+
world state, and a resource or column a system did not declare is refused
|
|
19
|
+
with the system and the record named, so the fix is one line. A third,
|
|
20
|
+
The Kestrel Stair (`examples/kestrel-stair`, 3D third-person), restored
|
|
21
|
+
`world.snapshot()` after a relaunch with `world.restore()`: 93 entities
|
|
22
|
+
came back with their cloth, navmesh paths and behaviour state, and a full
|
|
23
|
+
session in two processes ended on one hash.
|
|
24
|
+
|
|
25
|
+
## 1. Game code is TypeScript, and only TypeScript
|
|
26
|
+
|
|
27
|
+
A game is `src/*.ts` against `@origingame/engine`. Never add Rust, a second
|
|
28
|
+
engine, a WebGL context, DOM access or a `<canvas>` of your own: the engine
|
|
29
|
+
renders, the shell owns the page. Anything that would need engine-side code
|
|
30
|
+
is an engine feature request, not something to work around in the game.
|
|
31
|
+
|
|
32
|
+
## 2. The tick is fixed and the world is deterministic
|
|
33
|
+
|
|
34
|
+
- Simulation runs in fixed ticks (`GameDesc.tick`, 60 Hz by default). A
|
|
35
|
+
system sees `time.dt`, `time.tick` and `time.elapsed` from the `Time`
|
|
36
|
+
resource; these are functions of the tick count.
|
|
37
|
+
- Never read `Date`, `performance.now()` or any other clock inside a system,
|
|
38
|
+
a command or a facts function. Two runs with the same seed and the same
|
|
39
|
+
inputs must produce the same world hash; a wall-clock read breaks that.
|
|
40
|
+
- Randomness comes from the `Rng` resource (`world.resource(og.Rng)`) or a
|
|
41
|
+
stream forked from it with a label (`rng.fork("spawner")`). Never
|
|
42
|
+
`Math.random()`. The project seed lives in `project.ogt` and `GameDesc.seed`;
|
|
43
|
+
`og run --seed` overrides it for a replay. A draw (`next`, `range`,
|
|
44
|
+
`integer`, `chance`) moves the stream, so it needs `og.Rng` in `writes`;
|
|
45
|
+
`fork` reads the seed alone and moves nothing, so a system that only
|
|
46
|
+
forks declares `og.Rng` in `reads` (its forks are the same on every host
|
|
47
|
+
and in every system).
|
|
48
|
+
- The wall clock reaches the game only as host input, between ticks:
|
|
49
|
+
`og.platform.now(world)` (UTC ms, the host's clock when the platform
|
|
50
|
+
attached or last answered, advanced by the ticks since) and the `time`
|
|
51
|
+
on every og.platform answer. A replay records them like a key press.
|
|
52
|
+
- Rendering never feeds back into simulation: do not read camera or screen
|
|
53
|
+
state to decide game logic.
|
|
54
|
+
- The engine may spread its own work over the cores (physics, skinning,
|
|
55
|
+
culling: the lanes), and it gives the same result to the bit as one
|
|
56
|
+
thread. Game systems always run on the engine thread, one after another,
|
|
57
|
+
so nothing in game code needs a lock; it needs only the rules above.
|
|
58
|
+
|
|
59
|
+
### Saving progress
|
|
60
|
+
|
|
61
|
+
The world is one snapshot: `world.snapshot()` returns it as text
|
|
62
|
+
(`og-save:1:...`: every entity, component, name, resource, rng state,
|
|
63
|
+
pending event and module state, with each type's schema hash) in every
|
|
64
|
+
build, publish included, and `world.restore(save)` puts it back, so the
|
|
65
|
+
restored world hashes as the saved one did, on either host. The session
|
|
66
|
+
stays across a restore (input, the player's volumes, the platform's calls,
|
|
67
|
+
the language, scheduled command steps); og.Time takes the saved tick.
|
|
68
|
+
Inside a system the snapshot is the world in the middle of that tick, as
|
|
69
|
+
that system sees it (its own queued spawns and despawns are not in it),
|
|
70
|
+
and it names the system: the world restored from it completes the tick on
|
|
71
|
+
its next step (the systems after that one, then the engine's, physics
|
|
72
|
+
included) and then goes on tick for tick as the run that saved did, so a
|
|
73
|
+
replay from it matches. Make the snapshot the system's last write of the
|
|
74
|
+
tick. A restore asked for in a system applies when the tick ends. A save whose types' fields changed is refused naming them, or
|
|
75
|
+
mapped by name with `{ migrate: true }`. Only what is in the world is
|
|
76
|
+
saved: progress in module variables is lost. The text is a few tens of
|
|
77
|
+
KiB (11 KiB for a few entities, 25 KiB for a 2D level of 189, 78 KB for a
|
|
78
|
+
3D world of 93 with cloths, minds and skinned characters), often over the
|
|
79
|
+
store's 64 KiB a key: save it with `og.platform.saveSlot(world, { slot,
|
|
80
|
+
text, meta })`, which spreads it over as many keys as it needs and writes
|
|
81
|
+
the slot's manifest last, so a save cut short leaves the last one whole,
|
|
82
|
+
and read it back with `og.platform.loadSlot` (og-game §17, og-platform-sdk
|
|
83
|
+
§2).
|
|
84
|
+
|
|
85
|
+
### Recording, replaying and ghosts
|
|
86
|
+
|
|
87
|
+
The engine has no in-game recorder: determinism is the recorder. The same
|
|
88
|
+
world and the same inputs give the same run to the tick, so a game records
|
|
89
|
+
its own inputs and replays them. Three things are called "replay": the
|
|
90
|
+
probe's and playtests' re-runs (`og run --commands`, `--seed`: tests,
|
|
91
|
+
og-build §4), a **watch replay** (the player's run driven again) and a
|
|
92
|
+
**ghost** (a past run drawn beside the player). The last two are game code;
|
|
93
|
+
examples/meridian-loop does both (`src/replay.ts`, `src/tape.ts`,
|
|
94
|
+
`src/tapes.ts`, `src/ghost.ts`; `tests/replay.og.ts` checks the replay
|
|
95
|
+
crosses the line on the race's own ticks).
|
|
96
|
+
|
|
97
|
+
Watch replay: snapshot the start, tape the inputs, restore and feed the tape.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// 1. The start, taken by the FIRST system of a tick (later in a tick, the
|
|
101
|
+
// restored world skips the rest of that tick and runs a different race).
|
|
102
|
+
r.replayMark = true; // saved set: the restored world knows it is a replay
|
|
103
|
+
const snap = world.snapshot();
|
|
104
|
+
r.replayMark = false;
|
|
105
|
+
const asked = snap.ok ? og.platform.saveSlot(world, { slot: "replay", text: snap.value }) : snap;
|
|
106
|
+
if (asked.ok) r.replaySave = asked.value; // its answer comes on a later tick (og.platform.result)
|
|
107
|
+
|
|
108
|
+
// 2. Every tick: the intent drives the car only through its quantised code,
|
|
109
|
+
// live and replayed alike, so the tape reproduces it exactly.
|
|
110
|
+
const code = r.replaying ? codeAt(tape, r.runLen) : encode({ steer, throttle, brake });
|
|
111
|
+
const i = decode(code); // steer 33 steps, throttle 17, brake 5: 12 bits
|
|
112
|
+
world.set(car, og.Vehicle, { steer: i.steer, throttle: i.throttle, brake: i.brake });
|
|
113
|
+
if (!r.replaying) putCode(tape, r.runLen, code); // two characters a tick
|
|
114
|
+
r.runLen += 1;
|
|
115
|
+
|
|
116
|
+
// 3. At the finish: the tape beside the start.
|
|
117
|
+
og.platform.storeSet(world, { key: "replay.tape", value: { ticks: r.runLen, inputs: tapeText } });
|
|
118
|
+
|
|
119
|
+
// 4. Watch replay: loadSlot, then world.restore(text) when it answers (it
|
|
120
|
+
// applies when the tick ends). The next tick sees replayMark set: clear
|
|
121
|
+
// it, set replaying, read "replay.tape" back, feed it from tick 0.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Keep the tape and the replay's flags in components and resources (a
|
|
125
|
+
restore rolls back everything else). A component has no array field and a
|
|
126
|
+
`str` field holds at most 4,096 bytes, so a tape of a few thousand ticks
|
|
127
|
+
is cut into `str` chunks, one entity each, written through the column's
|
|
128
|
+
bytes by the system that owns them (§4). The start is the whole world:
|
|
129
|
+
triangle-mesh colliders make it large (a 956 m circuit's world is 400 KB),
|
|
130
|
+
so it goes through `saveSlot`, which spreads it over the store's keys.
|
|
131
|
+
Request numbers are not carried across a restore: ask the store again
|
|
132
|
+
after it.
|
|
133
|
+
|
|
134
|
+
A ghost: record poses, draw a model. While a lap runs, tape the car's pose
|
|
135
|
+
every second tick (position and yaw, pitch, roll: 15 characters); from the
|
|
136
|
+
next lap draw a model with no body where the best lap's car was at the
|
|
137
|
+
same time into its lap, between the two poses around it:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
if (into % 2 === 0) putPose(cur, into / 2, { ...car.position, ...euler(car.quaternion) });
|
|
141
|
+
const f = into / 2, i0 = Math.floor(f), u = f - i0;
|
|
142
|
+
const a = poseAt(best, i0), b = poseAt(best, i0 + 1);
|
|
143
|
+
world.set(ghost, og.Transform, {
|
|
144
|
+
position: { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u, z: a.z + (b.z - a.z) * u },
|
|
145
|
+
quaternion: quat(lerpAngle(a.yaw, b.yaw, u), lerpAngle(a.pitch, b.pitch, u), lerpAngle(a.roll, b.roll, u)),
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Do not re-drive a second physics body from the tape for a ghost: it takes
|
|
150
|
+
part in the simulation the player drives in (a second og.Vehicle's wheel
|
|
151
|
+
rays land on whatever collider is under them, the player's car included),
|
|
152
|
+
so the player's run is no longer the one taped.
|
|
153
|
+
|
|
154
|
+
## 3. Every failure is a value
|
|
155
|
+
|
|
156
|
+
Engine calls that can fail return `Result<T>`: `{ ok: true, value }` or
|
|
157
|
+
`{ ok: false, reason }`. Check `ok` before reading `value`; the `reason` is a
|
|
158
|
+
sentence that names the fix. A dead handle is a refusal, never a crash. Do not
|
|
159
|
+
wrap engine calls in `try`/`catch` to hide refusals; handle the `reason`.
|
|
160
|
+
|
|
161
|
+
## 4. Systems work on archetypes, never on single entities
|
|
162
|
+
|
|
163
|
+
A system declares what it reads and writes and receives the matching
|
|
164
|
+
archetypes once per tick:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
export const Move = defineSystem("Move", { reads: [Velocity], writes: [og.Transform] }, (q, time) => {
|
|
168
|
+
for (const a of q.archetypes()) {
|
|
169
|
+
const p = a.column(og.Transform).position; // Float32Array, stride 3
|
|
170
|
+
const v = a.column(Velocity).v;
|
|
171
|
+
for (let i = 0; i < a.count; i++) {
|
|
172
|
+
p[i * 3] += v[i * 3] * time.dt;
|
|
173
|
+
p[i * 3 + 1] += v[i * 3 + 1] * time.dt;
|
|
174
|
+
p[i * 3 + 2] += v[i * 3 + 2] * time.dt;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Columns are typed arrays over engine memory. Write only components listed
|
|
181
|
+
in `writes`; reading needs `reads` or `writes`. An undeclared column throws
|
|
182
|
+
inside the system and fails the tick with the system's name.
|
|
183
|
+
- The same holds for resources and events: `world.resource`, an event read
|
|
184
|
+
(`world.events`) and an `og.Rng` fork need the record among `reads` or
|
|
185
|
+
`writes`; a resource write, an `og.Rng` draw and `world.emit` need it in
|
|
186
|
+
`writes`. Undeclared, it throws in `og run` and `og test` (dev and
|
|
187
|
+
release builds) with the system and the record; a publish build refuses
|
|
188
|
+
it, logs it once per system and access, and the game runs on.
|
|
189
|
+
- A resource handle is a typed view of the resource's packed record:
|
|
190
|
+
`world.resource(R)` hands back the same handle in a world every time, and
|
|
191
|
+
a field read or write is a typed-array access (about 0.02 µs, whatever
|
|
192
|
+
the resource's size; a 130-field run state costs what an 11-field one
|
|
193
|
+
does). The record is fetched again, in one engine call answering its
|
|
194
|
+
bytes (no JSON), only by the first read after another engine call
|
|
195
|
+
(`world.*`, `og.*`) or a system boundary, the only times the engine can
|
|
196
|
+
change it; writes are kept and written back in one call before the next
|
|
197
|
+
engine call and when the system or command returns. So two handles
|
|
198
|
+
always agree, a read after an engine call sees the engine's value, and
|
|
199
|
+
the world hash is what writing field by field gives. The rule: keep the
|
|
200
|
+
run in one resource and read and write its fields where they are used;
|
|
201
|
+
no plain copies, no handle passed around to save a fetch. What costs is
|
|
202
|
+
alternating an engine call with a read of a resource the system wrote
|
|
203
|
+
(one write-back and one fetch each time, a few µs): in a hot loop over
|
|
204
|
+
entities, read the fields into locals before the loop's engine calls.
|
|
205
|
+
- Views belong to one system call. Never keep an `ArchetypeView` or a column
|
|
206
|
+
array across ticks or in module state: a stale view throws with the tick
|
|
207
|
+
and the system it belonged to.
|
|
208
|
+
- Text is world state like a number: a `str(capacity)` field holds UTF-8 of
|
|
209
|
+
at most `capacity` bytes inline, zero-padded (`name: str(48)` takes any 12
|
|
210
|
+
characters, four bytes covering every script). `world.get`, `world.set`
|
|
211
|
+
and a resource's fields move it as a JS string; text over the capacity is
|
|
212
|
+
refused with the fix named (an error value from `world.insert`/`world.set`,
|
|
213
|
+
a throw from a resource's setter), never truncated. Its column is the raw
|
|
214
|
+
bytes (`Uint8Array`, stride the capacity). The probe reads it (`get:`,
|
|
215
|
+
`dump_entity`), `set:Prefs.name=Ada Lovelace` writes it (a JSON literal for
|
|
216
|
+
escapes: `set:Prefs.name=""`), a scene gives it one quoted string, and
|
|
217
|
+
snapshots and the world hash carry its bytes, so a name never lives in a
|
|
218
|
+
module variable or as numbers of code points.
|
|
219
|
+
A `str` holds 1 to 4,096 bytes: a larger capacity typechecks and is
|
|
220
|
+
refused when the game starts (`a string field holds 1 to 4096 bytes`).
|
|
221
|
+
- A field holds one value: a component has no array or list field. A few
|
|
222
|
+
values are numbered fields (`p0`..`p7`, as og.CameraRail's rail points);
|
|
223
|
+
a list is an entity per element with its own component, looped over by
|
|
224
|
+
a query (a ring of skid marks, the laps of a race); a long record (a
|
|
225
|
+
lap's taped inputs) is text in `str(4096)` chunks, an entity each.
|
|
226
|
+
- A handle is a 64-bit number (an `og.load` answer, a texture, a sound,
|
|
227
|
+
a geometry, a material, an entity): keep it in a field of its own kind,
|
|
228
|
+
`Asset()`, `Texture()`, `Sound()`, `Geometry()`, `Material()` or
|
|
229
|
+
`Entity()` (each handle type of the API has its factory), in a
|
|
230
|
+
component or a resource. A `u32` field refuses one (`4294967298 is not
|
|
231
|
+
a u32`).
|
|
232
|
+
- Per-entity calls (`world.get`, `world.set`) are for commands, setup and
|
|
233
|
+
the few named entities a system touches (a camera, a listener, the
|
|
234
|
+
player), not for hot loops. A system's `reads` and `writes` borrow
|
|
235
|
+
nothing by themselves: a column is borrowed when the system takes its
|
|
236
|
+
view (`a.column(C)`), and held until the system returns. So `world.get`
|
|
237
|
+
and `world.set` reach any entity its declarations match until the system
|
|
238
|
+
takes that archetype's column of the component; after it, they refuse
|
|
239
|
+
(naming the view), and the entity is read and written through the view's
|
|
240
|
+
arrays at its row (`a.entities`). Take per-entity calls first, views
|
|
241
|
+
after, or keep to one of the two in a system.
|
|
242
|
+
- A system has one query: every component in its `reads` and `writes`
|
|
243
|
+
together, so an entity matches only when it has all of them (and none of
|
|
244
|
+
`without`). A component declared only to `world.get` one named entity
|
|
245
|
+
(the player's car) narrows the loop to entities that also have it, often
|
|
246
|
+
to none, with no error. `world.query` is refused inside a system, by
|
|
247
|
+
design (a system reaches only what it declared): loop over each
|
|
248
|
+
archetype in a system of its own that declares only its components, and
|
|
249
|
+
pass what crosses between them through a resource.
|
|
250
|
+
- Order systems with `after` and `before` by name; declaration order is the
|
|
251
|
+
default.
|
|
252
|
+
|
|
253
|
+
## 5. Reflection is the one schema
|
|
254
|
+
|
|
255
|
+
A component or resource is declared once with `defineComponent` /
|
|
256
|
+
`defineResource` and its column factories (`f32(100, { min: 0, max: 1000,
|
|
257
|
+
tunable: true })`, `Vector3()`). That one layout is what scenes, dumps, the
|
|
258
|
+
probe's `bounds` and `patch`, Tune and the wiki read. Never describe the same
|
|
259
|
+
data a second time (a JSON config mirroring a component, a hand-written
|
|
260
|
+
schema for the inspector). Put ranges, units and `tunable` on the field where
|
|
261
|
+
it is declared.
|
|
262
|
+
|
|
263
|
+
## 6. Desc objects and names
|
|
264
|
+
|
|
265
|
+
- Desc objects (`SpawnDesc`, `SystemDesc`, `GameDesc`) may omit any field;
|
|
266
|
+
the zero value or the documented default applies. Pass `{}` rather than
|
|
267
|
+
inventing values the engine already defaults.
|
|
268
|
+
- Names follow the API: components, resources and systems PascalCase;
|
|
269
|
+
command actions snake_case; entity names are stable strings, because the
|
|
270
|
+
probe and the command channel address entities by them.
|
|
271
|
+
- Vector, quaternion, matrix, colour, transform and material vocabulary is
|
|
272
|
+
three.js's (`position`, `quaternion`, `scale`, `metalness`, `roughness`).
|
|
273
|
+
|
|
274
|
+
## 7. Where the API is described
|
|
275
|
+
|
|
276
|
+
The generated wiki (`reference/index.md` next to the `og-game` skill, or
|
|
277
|
+
`wiki/index.md` in `@origingame/engine-knowledge`) has one page per module
|
|
278
|
+
with every signature quoted from the IDL, grouped by the skill that teaches
|
|
279
|
+
it. This skill's are the ECS and its rules: `reference/world.md` (entities,
|
|
280
|
+
queries, resources, events), `reference/system.md` (systems and their
|
|
281
|
+
order), `reference/reflect.md` (components, resources and their column
|
|
282
|
+
factories), `reference/rng.md` (seeded streams) and `reference/math.md`
|
|
283
|
+
(vectors, quaternions, colours as data). When the wiki and memory disagree,
|
|
284
|
+
the wiki is right: it is regenerated from the same IDL the engine is built
|
|
285
|
+
from. The other skills, and which modules each teaches, are og-game §1.
|