docs(workspace): backfill architecture decision records 0002-0006

This commit is contained in:
Serkyo 2026-06-28 00:46:47 +02:00
parent 1b8dcdced3
commit 0416aa35ca
7 changed files with 131 additions and 4 deletions

View file

@ -6,7 +6,7 @@ Guidance for AI coding agents — and human contributors — working in this rep
Voxel-based game with souls-like combat. Built in Rust; rendering targets Vulkan via [`ash`](https://github.com/ash-rs/ash) (raw Vulkan bindings, not a higher-level wrapper like `wgpu` or `vulkano`). Voxel-based game with souls-like combat. Built in Rust; rendering targets Vulkan via [`ash`](https://github.com/ash-rs/ash) (raw Vulkan bindings, not a higher-level wrapper like `wgpu` or `vulkano`).
World is procedurally generated. Voxel edge length is 0.5 m, so the player occupies **3 blocks tall × 2 blocks wide**. This finer grid is load-bearing for design decisions: collision, mesh chunking, LOD thresholds, and network bandwidth all need to assume ~8× the voxel count of a 1 m-grid world per unit volume — pick chunk sizes and data layouts accordingly. Supports both single-player and multiplayer via a dedicated server — that dual mode is why `server` exists as its own crate even for solo play (the single-player path is expected to run the server logic in-process or invoke the same crate, rather than having a separate offline code path). World is procedurally generated. Voxel edge length is 0.5 m, so the player occupies **3 blocks tall × 2 blocks wide**. This finer grid is load-bearing for design decisions: collision, mesh chunking, LOD thresholds, and network bandwidth all need to assume ~8× the voxel count of a 1 m-grid world per unit volume — pick chunk sizes and data layouts accordingly. The rationale and trade-offs of this scale are recorded in [ADR-0002](docs/adr/0002-half-scale-voxel-grid.md). Supports both single-player and multiplayer via a dedicated server — that dual mode is why `server` exists as its own crate even for solo play (the single-player path is expected to run the server logic in-process or invoke the same crate, rather than having a separate offline code path).
## Documentation map ## Documentation map
@ -43,6 +43,8 @@ This has hard implications when adding features:
- Authoritative APIs (world mutation, combat resolution) are defined in `scripting` but gated so the client-side Lua VM can't invoke them. One API surface, two execution contexts: client VM = read-only/UI/effects, server VM = authoritative. - Authoritative APIs (world mutation, combat resolution) are defined in `scripting` but gated so the client-side Lua VM can't invoke them. One API surface, two execution contexts: client VM = read-only/UI/effects, server VM = authoritative.
- Prefer wrapper newtypes inside `scripting` over `impl UserData for SharedType` in `shared`, to avoid coupling the protocol crate to `mlua`. - Prefer wrapper newtypes inside `scripting` over `impl UserData for SharedType` in `shared`, to avoid coupling the protocol crate to `mlua`.
The decision to build the base game on top of the modding API, and the client/server VM gating that follows from it, are recorded in [ADR-0006](docs/adr/0006-base-game-on-modding-api.md).
## Assets ## Assets
All game assets live under `/assets` at the repo root, organised into subfolders by kind: `icons/`, `models/`, `shaders/`, `sounds/`, `textures/`, `scripts/`. New assets must be placed in the matching subfolder — do not drop loose files into `/assets` itself, and do not scatter assets inside crate directories. All game assets live under `/assets` at the repo root, organised into subfolders by kind: `icons/`, `models/`, `shaders/`, `sounds/`, `textures/`, `scripts/`. New assets must be placed in the matching subfolder — do not drop loose files into `/assets` itself, and do not scatter assets inside crate directories.
@ -144,13 +146,13 @@ The game is **multithreaded by design** — single-threaded would not meet the p
## Determinism stance ## Determinism stance
- **Worldgen is seed-deterministic.** Given the same seed, worldgen must produce bit-for-bit the same world on any platform, any time. This constrains worldgen code: use a fixed RNG algorithm (e.g. `wyrand`, `xoshiro`) — **never** `rand::thread_rng()` or anything seeded from the OS. Do not depend on `HashMap` iteration order (Rust's default hasher is randomised); use `BTreeMap`, `IndexMap`, or sort explicitly when iteration order feeds into RNG draws or content placement. - **Worldgen is seed-deterministic.** Given the same seed, worldgen must produce bit-for-bit the same world on any platform, any time. This constrains worldgen code: use a fixed RNG algorithm (e.g. `wyrand`, `xoshiro`) — **never** `rand::thread_rng()` or anything seeded from the OS. Do not depend on `HashMap` iteration order (Rust's default hasher is randomised); use `BTreeMap`, `IndexMap`, or sort explicitly when iteration order feeds into RNG draws or content placement. See [ADR-0003](docs/adr/0003-seed-deterministic-worldgen.md).
- **Simulation is server-authoritative.** The server runs the truth; clients send inputs and receive state snapshots, predicting locally for responsiveness and reconciling on disagreement. Combat, physics, mob AI, and item drops are computed once, on the server. - **Simulation is server-authoritative.** The server runs the truth; clients send inputs and receive state snapshots, predicting locally for responsiveness and reconciling on disagreement. Combat, physics, mob AI, and item drops are computed once, on the server.
- **Full simulation determinism (lockstep / rollback / replay-from-inputs) is a non-goal.** This means floats, hash-map iteration, and platform-specific math are all fair game *outside of worldgen*. Don't pay the cost of cross-platform float reproducibility for a feature we're not building. - **Full simulation determinism (lockstep / rollback / replay-from-inputs) is a non-goal.** This means floats, hash-map iteration, and platform-specific math are all fair game *outside of worldgen*. Don't pay the cost of cross-platform float reproducibility for a feature we're not building. See [ADR-0004](docs/adr/0004-server-authoritative-simulation.md).
## Content IDs & namespacing ## Content IDs & namespacing
All registered content (blocks, items, recipes, biomes, entities, …) is identified by a **namespaced string** of the form `"namespace:id"`. All registered content (blocks, items, recipes, biomes, entities, …) is identified by a **namespaced string** of the form `"namespace:id"`. The full rationale is in [ADR-0005](docs/adr/0005-namespaced-content-ids.md).
- **Engine's reserved namespace:** `core:`. All first-party content registered by the base game uses it (`"core:stone"`, `"core:iron_sword"`). Mods pick their own short namespace (`"mymod:weird_dirt"`). - **Engine's reserved namespace:** `core:`. All first-party content registered by the base game uses it (`"core:stone"`, `"core:iron_sword"`). Mods pick their own short namespace (`"mymod:weird_dirt"`).
- **Strict form required.** A bare ID with no `:` is an **error at registration / parse time**, not silently coerced to `core:`. Same rule everywhere: engine scripts, data packs, Lua mods, recipe references, save files. No exceptions — the symmetry is the point. - **Strict form required.** A bare ID with no `:` is an **error at registration / parse time**, not silently coerced to `core:`. Same rule everywhere: engine scripts, data packs, Lua mods, recipe references, save files. No exceptions — the symmetry is the point.

View file

@ -31,6 +31,11 @@ Each subsystem note should name the design topic it implements (by title, e.g. "
- [`adr/`](adr/) — Architecture Decision Records. - [`adr/`](adr/) — Architecture Decision Records.
- [`adr/0001-record-architecture-decisions.md`](adr/0001-record-architecture-decisions.md) — establishes the ADR practice. - [`adr/0001-record-architecture-decisions.md`](adr/0001-record-architecture-decisions.md) — establishes the ADR practice.
- [`adr/0002-half-scale-voxel-grid.md`](adr/0002-half-scale-voxel-grid.md) — the half-scale voxel grid.
- [`adr/0003-seed-deterministic-worldgen.md`](adr/0003-seed-deterministic-worldgen.md) — seed-deterministic worldgen.
- [`adr/0004-server-authoritative-simulation.md`](adr/0004-server-authoritative-simulation.md) — server-authoritative simulation.
- [`adr/0005-namespaced-content-ids.md`](adr/0005-namespaced-content-ids.md) — namespaced content IDs.
- [`adr/0006-base-game-on-modding-api.md`](adr/0006-base-game-on-modding-api.md) — base game built on the modding API.
- [`adr/template.md`](adr/template.md) — template for new decisions. - [`adr/template.md`](adr/template.md) — template for new decisions.
Subsystem notes are added here as systems are implemented and locked. Subsystem notes are added here as systems are implemented and locked.

View file

@ -0,0 +1,21 @@
# 0002. Half-scale voxel grid
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
The world is built from voxels. The physical edge length chosen for a voxel propagates into nearly every system: collision resolution, mesh chunking, level-of-detail thresholds, network bandwidth, and in-memory data layout all scale with voxel density. A finer grid yields more expressive terrain and building at a direct cost in voxel count per unit volume.
Relative to a conventional coarse voxel grid with a 1 m edge, halving the edge to 0.5 m doubles the linear resolution and multiplies the voxel count per unit volume by roughly 8×.
## Decision
A voxel edge is **0.5 m**. Inside the engine the world unit is the block itself: **1 unit = 1 block**, counted in blocks rather than metres. A player therefore occupies **3 units tall × 2 units wide** (3 blocks × 2 blocks).
## Consequences
- Density is ~8× that of a 1 m-grid world per unit volume. Chunk dimensions and voxel data layouts must be chosen with that multiplier in mind; a layout that is comfortable on a coarse grid may not be here.
- Collision, meshing, LOD selection, and network bandwidth budgets all inherit the 8× factor and must be designed against it from the start.
- Finer terrain and construction detail become possible, this is the motivating benefit.
- The choice is load-bearing and expensive to revisit later, since persisted worlds and save formats encode the block scale.

View file

@ -0,0 +1,23 @@
# 0003. Seed-deterministic worldgen
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
Procedural world generation must be reproducible: given the same seed, the same world is expected on any platform and at any time. Reproducibility enables shared seeds, reliable bug reproduction, and consistent behaviour between a server and any client that regenerates terrain locally.
Reproducibility is fragile. Sources of nondeterminism include OS-seeded random number generators, the randomised iteration order of the standard hasher, and platform-specific arithmetic.
## Decision
Worldgen is **seed-deterministic** and is held to bit-for-bit reproducibility across platforms.
- A fixed RNG algorithm (e.g. `wyrand`, `xoshiro`) is used, seeded only from the world seed. `rand::thread_rng()` and any OS-seeded source are prohibited in worldgen.
- Iteration order that feeds RNG draws or content placement must be deterministic. The default randomised-hash `HashMap` iteration order must not be relied upon; use `BTreeMap`, `IndexMap`, or an explicit sort.
## Consequences
- Worldgen code is constrained in its choice of RNG and collection types, and reviewers must watch for nondeterministic iteration order.
- Identical worlds are guaranteed from identical seeds, on any supported platform.
- This guarantee is scoped to worldgen only; see [ADR-0004](0004-server-authoritative-simulation.md) for why the rest of the simulation is deliberately not held to the same standard.

View file

@ -0,0 +1,23 @@
# 0004. Server-authoritative simulation
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
The game supports single-player and multiplayer through one dedicated `server` crate; single-player runs that same server logic rather than a separate offline path. A networked simulation must decide where authority lives and how much determinism the simulation is held to.
One option is full simulation determinism (lockstep, rollback, or replay-from-inputs), which permits clients to advance the simulation in agreement and exchange only inputs. It is powerful but imposes a heavy, ongoing cost: every float, every hash-map iteration, and all platform-specific math must be made cross-platform reproducible.
## Decision
The simulation is **server-authoritative**. The server computes the truth; clients send inputs and receive state snapshots, predicting locally for responsiveness and reconciling on disagreement. Combat, physics, mob AI, and item drops are computed once, on the server.
**Full simulation determinism (lockstep / rollback / replay-from-inputs) is an explicit non-goal.** Outside of worldgen (see [ADR-0003](0003-seed-deterministic-worldgen.md)), floats, hash-map iteration order, and platform-specific math are all permitted.
## Consequences
- The engine does not pay the cost of cross-platform float reproducibility for the simulation — only worldgen carries that burden.
- Clients require prediction and reconciliation logic to stay responsive against an authoritative server.
- Cheat resistance follows from authority residing on the server.
- Features that would require deterministic replay of the full simulation are out of scope by this decision; reopening them would mean revisiting the determinism cost deliberately avoided here.

View file

@ -0,0 +1,26 @@
# 0005. Namespaced content IDs
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
All registered content — blocks, items, recipes, biomes, entities, and so on — needs a stable identifier that is unambiguous across the engine, data packs, Lua mods, recipe references, and save files. First-party and third-party content must coexist without collision, and identifiers must survive being written to disk and read back.
## Decision
Content is identified by a **namespaced string** of the form `"namespace:id"`.
- The namespace `core:` is reserved for first-party content (`"core:stone"`, `"core:iron_sword"`). Mods choose their own short namespace (`"mymod:weird_dirt"`).
- The strict form is mandatory. A bare id with no `:` is an **error at registration / parse time**, never silently coerced to `core:`. The rule is identical everywhere: engine scripts, data packs, Lua mods, recipe references, save files.
- Charset: namespace and id are each `[a-z0-9_-]+` with exactly one `:` between them. Lowercase ASCII only — no uppercase, Unicode, spaces, dots, or slashes.
- At registration time each id string is interned into a small integer handle (e.g. `BlockId(u32)`). Hot paths compare handles; the original string is kept for display, save/load, and the Lua API.
The reserved namespace is deliberately `core:` rather than the project name, so it remains stable independent of branding.
## Consequences
- Identifiers are greppable, filesystem-safe, and unambiguous in logs and save files.
- First-party and mod content cannot collide, and the absence of a silent default means mistakes surface immediately rather than corrupting data.
- A small runtime cost is paid at registration to intern strings, in exchange for handle comparison on hot paths.
- The strict form is a permanent contract: relaxing it later (e.g. defaulting bare ids) would change the meaning of existing save files and is therefore effectively irreversible.

View file

@ -0,0 +1,27 @@
# 0006. Base game built on the modding API
- **Status:** Accepted
- **Date:** 2026-06-27
## Context
The engine exposes a Lua modding API. A modding API can be treated as a bolt-on layer over a separate, privileged engine path, or the engine's own content can be defined through the same API that mod authors use. The former tends to let the engine drift ahead of the API, leaving mod authors with second-class capabilities and no working reference.
The API is also exposed to two execution contexts — a client-side Lua VM and a server-side Lua VM — with different trust levels. Authoritative operations (world mutation, combat resolution) must not be invocable from the client VM.
## Decision
The base game is built on top of the modding API; the shipped content (blocks, items, entities, recipes, …) is defined through the same API mod authors use, so it doubles as reference material.
- Any new gameplay primitive must be reachable through the Lua API, not only through a Rust-internal path. Adding a Rust-side concept with no API surface breaks the dogfooding contract.
- The API and its bindings live in the `scripting` crate, which owns the `mlua` dependency, the API table registration, and the mod loader. Both `client` and `server` depend on it; `shared` does not.
- Authoritative APIs are defined once but **gated by execution context**: the client VM is restricted to read-only / UI / effects, while the server VM holds authority. One API surface, two contexts.
- `scripting` wraps `shared` types in newtypes rather than implementing `UserData` for them in `shared`, keeping the protocol/data crate free of `mlua`.
## Consequences
- Mod authors can read first-party content as a faithful example of what the API allows, because it uses no privileged path they lack.
- The API must stay stable and discoverable, since it is both the engine's and the modder's surface; engine internals must not leak through it.
- The client/server trust boundary is enforced at the API layer rather than re-checked ad hoc.
- A feature cannot be "added to the engine" and exposed to mods later as an afterthought — the API surface is part of the definition of done.
- Declarative content loading follows the same single-path rule; see [ADR-0007](0007-declarative-content-via-modding-api.md).