openvibers/openvibe.games ARCHITECTURE.md

ARCHITECTURE.md

main 8.3 KB · 156 lines · markdown

1# Architecture
2
3## Principles
4
51. **The server is the game.** Clients render, predict, and express intent;
6 every gameplay-relevant decision (movement, inventory, crafting, placement,
7 physgun, damage, economy) is validated or simulated server-side. No client
8 field is ever trusted.
92. **Domain logic is engine-free.** `@openvibe/gameplay` has no Babylon, Rapier,
10 DOM, or network imports; it runs identically in vitest, on the server, and
11 inside client prediction. Engines live behind adapters.
123. **Data-driven content.** Items, recipes, and worlds are declarative
13 definitions validated at startup (`@openvibe/content`). Adding content never
14 requires engine changes. Stable ids everywhere; display names are cosmetic.
154. **Persistent state is explicit.** DTOs in `@openvibe/persistence` are the disk
16 format. Runtime objects (meshes, bodies, live inventories) are transient
17 projections rebuilt from them.
185. **Sleep is a first-class citizen.** Settled physics objects cost nothing on
19 the wire (excluded from snapshots) and nothing per-tick; they exist as
20 pinned state until something wakes them.
21
22## Package dependency graph
23
24```
25 shared
26 / | \
27 protocol content physics ──(rapier adapter: deterministic Rapier wasm)
28 \ | /
29 gameplay
30 |
31 persistence (DTOs; PostgreSQL impl behind repository interfaces; migrations)
32 |
33 apps/server apps/client
34 (authoritative) (Babylon render + prediction + HUD)
35```
36
37Only `apps/client` knows Babylon exists: ADR-0007 decision 2 removed the engine
38from the authoritative server, and an architecture audit
39(`apps/server/src/architectureAudit.audit.ts`) fails if `apps/server` imports
40`@babylonjs/*`. Only `apps/server` and `packages/persistence` know PostgreSQL
41(openvibe-sdk/db) exists. `packages/*` never import browser-only APIs: the
42physics adapter is Rapier, which runs identically in Node and the browser with no
43scene or engine, so there is no Node-only path to keep separate.
44
45## Simulation model
46
47- Fixed 30 Hz tick on the server (`FixedTimestep` also drives client
48 prediction). Snapshots at 15 Hz. Rendering is decoupled and interpolated.
49- **Movement** is a kinematic Source/Quake controller (`gameplay/movement`):
50 explicit velocity + capsule sweeps + clip-plane sliding + step-up, with
51 friction / ground-accelerate / air-accelerate (air-strafing works). The
52 player is _not_ a dynamic rigid body; a kinematic Rapier capsule mirrors the
53 player so props collide with them. The controller consumes a
54 `CollisionQueries` interface — the Rapier adapter provides it on both sides
55 (one implementation, no scene), the tests provide analytic worlds.
56- **Client prediction:** every input command (seq-stamped) is sent and applied
57 locally; snapshots carry the last processed seq; the client rewinds to the
58 authoritative state and replays unacked inputs. Remote entities render from
59 ~130 ms interpolation buffers.
60- **Physics:** Rapier (`@dimforge/rapier3d-deterministic-compat`, one pinned
61 build on both sides) behind `PhysicsWorld` (`@openvibe/physics`). Stepping is
62 always manual (`world.step(dt)` from the fixed loop) — never coupled to render
63 frames. Collision layers: Static / Prop / Player, filtered at the shape level
64 for rays and sweeps.
65
66## Networking
67
68- Explicit protocol package with a version constant; inbound messages are
69 zod-validated (range-clamped axes, size caps) and malformed traffic
70 disconnects. JSON encoding today behind an encode/decode boundary sized for
71 a binary codec swap.
72- **Interest management:** per-session cell subscriptions and known-entity
73 sets built from the spatial hash. A precise radius check filters each cell's
74 candidates; crossing cells sends spawn/despawn differences with exit-radius
75 hysteresis. Snapshots contain only relevant players + _awake_ relevant bodies.
76- **Sleep networking:** when a prop settles, one final `entity` pin is
77 broadcast and it drops out of snapshots; on wake it re-enters. Settled
78 props also mark themselves dirty exactly once for the persistence flush.
79- Wire-level rate limiting (messages/sec, payload bytes) plus a bounded input
80 queue per session (max 2 catch-up sims/tick) prevent client-driven speedup.
81
82## Server systems (per tick)
83
841. Drain input queues → movement simulation per session (never client
85 positions; silent clients coast to a stop after 3 bridged ticks)
862. Physgun drive: held bodies pulled toward the holder's view ray via
87 velocity control (never teleported) with proportional angular control
883. `physics.step(dt)`
894. Sync awake prop transforms into entity records; settle/wake transitions
905. Crafting queues (tick-based completion, output-space aware)
916. Replication (every 2nd tick): interest diff + snapshot per session
927. Periodic write-behind checkpoint (a dirty snapshot handed to the flusher; the tick never awaits I/O)
93
94## Persistence
95
96PostgreSQL 18 through openvibe-sdk/db, behind `PersistenceStore` repositories (the SDK's async handle
97and `sql` fragments; migrations in `packages/persistence/migrations/0001_initial.sql` applied at boot
98by the SDK migrator as the owner role). The schema: `places`, `world_meta`, `world_entities`
99(props/resources/NPCs with motion + kind-specific state JSON), `world_constraints`, `characters`
100(inventory, skills, friends, reputation, unlocks, stats, armor, active job), `mods`, `mod_grants`,
101`mod_placements`, `mod_audit`, `media_mirrors`, `identity_adoptions`, `account_data_events`, and the
102SDK's `event_outbox` and `token_revocations`. JSON columns are jsonb and round-trip as objects; a
103local guest account is keyed by `guest:` + sha256(their token), never the raw token.
104
105**The tick never awaits I/O.** It takes a copy-on-write snapshot of the dirty rows
106(`world.takeDirty()`, `npcs.takeDirty()`, dirty sessions, env/markets) and the write-behind flusher
107(`apps/server/src/game/flusher.ts`) persists it in one transaction, one flush in flight per instance.
108A checkpoint while one is running is coalesced (the dirty flags stay set), a failed transaction
109restores the snapshot's dirtiness and is retried, and shutdown drains the queue before the database
110closes. Batch writes are multi-row `INSERT … ON CONFLICT` in chunks of at most 500 rows. First boot
111seeds from the world definition; afterwards the database is the world's source of truth. The
112simulation never reads the platform tables.
113
114M4 runs several isolated simulations of the same place in one process. A PostgreSQL
115`game_instances` claim has a renewable lease version; state writes lock and verify
116that claim in their transaction. Later rooms keep separate world rows while
117characters and their inventory remain shared. Each character keeps a last
118position per room. See `docs/instances.md` for transfer and the process limit.
119
120## Platform boundary
121
122Adapters to the OpenVibe platform live in `apps/server/src/platform` and
123`apps/server/src/mods` (ADR-0006): canonical subject accounts, the `games`
124service principal, the events outbox, the Media mirror, and the mod registry
125with its runtime seam (`ModApi`: capability-checked bindings, the only thing a
126mod can reach). No package under `packages/` imports any of it.
127
128## Zones
129
130Declarative rule volumes (`pvp`, `build`, `physgun`) resolved by position;
131systems ask `rulesAt(pos)` — the protected city is future _content_, not code.
132
133## Entities
134
135`GameEntity` is a small record with optional capability components (prop,
136resource, owner, …) in an `EntityStore` with kind indexes. Deliberately not a
137full archetype ECS yet: systems only touch query methods, so storage can be
138migrated if profiling demands it. The same composition philosophy governs item
139definitions (`world`, `placeable`, `workstation` capability blocks).
140
141## Observability
142
143`ServerMetrics` sampled by the tick loop (tick/physics ms EMA, awake/settled
144bodies, sessions, bytes out, snapshot size, RSS), exposed at `/metrics`,
145summarized to structured JSON logs periodically. Logs carry system/player/
146entity context fields and never spam per-tick.
147
148## Security posture
149
150Assume hostile clients: protocol validation at the edge, range checks on every
151interaction (gather, place, physgun grab/unfreeze), zone rules, ownership
152recorded on placement, inventory ops validated against authoritative state,
153one live session per identity token, guest tokens restricted to a format
154that can never name an account key. Client-side rays
155exist purely for UX.
156