terainia/ARCHITECTURE.md
Maximus Gorog 8aafc7a939 Add ARCHITECTURE.md mapping the functional-core/imperative-shell split
Documents every module's responsibility, the tick pipeline, the
sim::lighting shared-truth invariant (sun direction shared between
Rust and WGSL via shader_source.rs), and the test coverage. Companion
to DEPLOY.md — that one covers ops, this one covers code.
2026-05-23 23:33:02 -06:00

140 lines
6.5 KiB
Markdown

# voxel-game architecture
Functional-core / imperative-shell split. Every game-logic
transformation is a pure function `(value, value) -> value`; the shell
threads them through the GPU + winit + JS bridges each tick.
## Module map
```
src/
├── lib.rs module declarations + run()
├── main.rs native binary entry
├── app.rs IMPERATIVE SHELL — App + winit handler + tick().
│ Composes sim/ + net/ + render/ through bridges.
├── bridges.rs THE ONLY MUTABLE-STATE OWNER. thread_local cells
│ for touch input, game status, network, settings,
│ plus the wasm-bindgen JS interface. Typed
│ accessor functions hide the RefCells.
├── camera.rs Camera + InputState + KbHeld (pure value types).
├── world.rs World + Chunk + Block + raycast + natural_surface_y.
├── mesh.rs Vertex + build_chunk_mesh (greedy meshing + AO);
│ emit_oriented_box + name_hash for remote players.
├── proto.rs Wire format for the multiplayer server.
├── shader.wgsl Terrain + sky + outline pipelines (WGSL).
├── post.wgsl Post-process pass-through.
├── shader_source.rs Assembles shader source by prepending Rust-
│ derived constants (DAY_PERIOD, SUN_OFFSET) to
│ shader.wgsl — one source of truth shared with
│ sim::lighting.
├── sim/ PURE SIMULATION CORE. No GPU, no winit, no JS.
│ ├── mod.rs re-exports
│ ├── body.rs PlayerBody { feet, velocity, hp, alive, ... }
│ ├── collision.rs AABB + sweep_axis(world, feet, delta, axis) →
│ │ (Vec3, bool)
│ ├── edit.rs apply_edit(world, EditRec) + chunks_for_edit
│ ├── event.rs SimEvent { Landed, VoidDeath, BlockEdited }
│ ├── input.rs TouchBridge + merge_held(kb, bridge) → KbHeld
│ ├── lighting.rs ⭐ SHARED SUN/SKY TRUTH ⭐
│ │ sun_direction(t), day_strength, twilight_amount,
│ │ LightingFrame::at(t), is_in_direct_sun(world, p, t).
│ │ Constants DAY_PERIOD / SUN_OFFSET exported to the
│ │ WGSL shader via shader_source.rs so the visual
│ │ sun direction can never disagree with the
│ │ mechanical one consumed by future mob burn /
│ │ plant growth / shade pathfinding.
│ ├── physics.rs step_movement(world, body, MoveInput) →
│ │ MoveOutcome { body', [SimEvent] }
│ ├── spawn.rs find_safe_spawn, fall_damage
│ └── visibility.rs compute_visible_chunks (frustum + radial cull)
├── net/ PURE NETWORK PARSING
│ └── mod.rs parse_inbox([raw line]) → [NetEvent]
└── render/ GPU SHELL — wgpu device, surface, pipelines
├── mod.rs Renderer struct + new() + all draw methods
├── pipelines.rs Pipeline + bind-group-layout factories
├── scene_target.rs depth / scene-color / post-bind-group helpers
└── uniform.rs CameraUniform (matches WGSL camera.frame)
+ OutlineVertex + ChunkBuffers
```
## Tick pipeline
`app::App::tick()` is one frame, in this order:
```
dt + scaled shader_time
(paused?) clear inputs, render last frame, return
drain touch bridge → per-tick input snapshot
honor respawn-request one-shot
drain net inbox → parse_inbox → fold NetEvents into world + remote-player map
send Hello on (re)connection
apply mouse look to camera (yaw/pitch)
sim::step_movement(world, body, MoveInput) → (body', [SimEvent])
fold sim events into damage
camera.position = body.feet + EYE_HEIGHT * Y
raycast → break/place → maybe broadcast edit
apply damage; periodic state broadcast
render_frame(settings, outline) → Renderer
```
The shell only mediates between pure morphisms and side-effecting
boundaries (GPU upload, network outbox, JS thread-locals).
## The lighting invariant
**Why this matters:** in Minecraft-class games it's common for the
visual shadow renderer and the "mob burns in sunlight" logic to use
*different* sun directions / different occlusion tests, and they drift.
A mob can stand in visible shade and burn anyway.
We avoid that by construction:
```
DAY_PERIOD, SUN_OFFSET (defined once in sim::lighting)
┌───────────┴───────────┐
▼ ▼
sim::lighting::sun_direction(t) shader_source::wgsl_constants_header()
│ │ (prepended to shader.wgsl)
▼ ▼
is_in_direct_sun(world, p, t) shader.wgsl::sun_direction(t)
│ │
▼ ▼
mob burn, plant growth, AI pathing visual shading (terrain + sky)
```
When real shadows land in the shader, they ray-march the same voxel
grid using the same `sun_direction(t)`. Visual shadow edges will
agree with mechanical sun edges by construction.
## Test coverage (53 tests as of this writing)
- `mesh`: 8 — winding correctness, greedy meshing, AO darkening, oriented-box winding
- `sim::body`: 3 — damage, immunity-when-dead, respawn reset
- `sim::collision`: 3 — air sweep, ground snap, no-clip invariant
- `sim::edit`: 2 — u8↔Block roundtrip + garbage fallback
- `sim::input`: 5 — merge_held release semantics (the joystick-stick bug)
- `sim::lighting`: 4 — night below horizon, lit at noon, occluded under overhead, ramp shapes
- `sim::physics`: 4 — gravity, jump-only-when-grounded, Landed event, void death, dead-body-doesn't-move
- `sim::spawn`: 7 — fall damage boundaries; spawn anchored to natural surface
- `net`: 4 — malformed dropped, Welcome / Edit / order
- `world`: 4 — terrain determinism, raycast hits/misses
- `shader_source`: 2 — header contains constants, terrain source contains body
- `app`: (no unit tests — orchestrator; coverage comes from the pure pieces it composes)
Running: `cargo test --lib` from the repo root.