docs(workspace): expand CLAUDE.md with project conventions

This commit is contained in:
Serkyo 2026-04-28 01:37:14 +02:00
parent bdc8d2a79b
commit 648024f494

View file

@ -97,6 +97,95 @@ The user is **learning** most of the stack used here (Rust, Vulkan/`ash`, `winit
In short: optimise for the user's understanding growing over time, not for the fastest path to working code.
## Concurrency model
The game is **multithreaded by design** — single-threaded would not meet the perf budget for voxel meshing, worldgen, rendering, networking, and simulation running together. Code should assume multiple threads and design data ownership accordingly:
- Prefer message-passing (channels: `crossbeam-channel`, `flume`, or `std::sync::mpsc`) and per-thread ownership over shared mutable state.
- When sharing is unavoidable, use the right primitive for the access pattern: `Arc<Mutex<_>>` for low-contention shared state, `Arc<RwLock<_>>` for read-heavy, atomics (`AtomicU32`, `AtomicBool`, …) for counters and flags, lock-free structures (`crossbeam`, `dashmap`) for hot paths. Avoid wrapping large hot data in a single `Mutex` "just in case" — that's how you accidentally serialise the whole engine.
- Worldgen and chunk meshing are the obvious parallelism wins. A thread pool (e.g. `rayon`, or a hand-rolled one) feeding meshing/generation jobs is expected.
- Vulkan command-buffer recording can be parallelised too, but Vulkan **queues** are not free-threaded — only one thread submits to a given queue at a time. Plan ownership of `vk::Queue` accordingly.
- The Lua VMs (one per execution context — client, server) are **not** thread-safe in `mlua`'s default config; treat each VM as owned by a single thread, and dispatch work to/from it via channels.
## Logging & error handling
- **Logging:** [`tracing`](https://docs.rs/tracing/) (with `tracing-subscriber` as the output backend). Use `info!` / `warn!` / `error!` / `debug!` / `trace!` macros at appropriate levels, and use **spans** (`#[tracing::instrument]`, `info_span!`) to scope work — they're how you keep multithreaded log output legible. Don't reach for `println!`/`eprintln!` for diagnostics; if it's worth printing, it's worth a `tracing` event.
- **Errors in libraries** (`shared`, `renderer`, `scripting`): typed error enums via [`thiserror`](https://docs.rs/thiserror/) (`#[derive(Error)]`). Each variant is a distinct, matchable failure mode. Don't expose `anyhow::Error` from a library API.
- **Errors in binaries** (`client`, `server`): [`anyhow`](https://docs.rs/anyhow/) at the top level, with `.context("...")` for human-readable layering. Library errors compose into `anyhow::Error` cleanly via `?`.
- **Never `.unwrap()` or `.expect()` outside `main` / setup / tests**, except where the invariant is genuinely impossible to violate. In the hot path, propagate with `?` and let the caller decide.
## Target platforms
**Linux and Windows only.** No macOS, no mobile, no console, no web/WASM.
- Both platforms have native Vulkan via vendor ICDs (NVIDIA / AMD / Intel). No translation layer (no MoltenVK story), so modern Vulkan extensions can be adopted freely without consulting a portability matrix.
- **File paths:** always use `std::path::Path` / `PathBuf` and the `directories` (or `dirs`) crate for user-data lookup. Never hard-code `/home/...` or `~`. Linux follows XDG (`$XDG_DATA_HOME` etc.); Windows uses `%APPDATA%`.
- **Line endings:** repo is LF-only. Set `core.autocrlf = false` and/or a `.gitattributes` with `* text eol=lf` to keep diffs clean across the two OSes.
- **Filename casing:** never have two files differing only in case. Linux is case-sensitive; Windows isn't; the mismatch produces confusing "works on my machine" bugs.
## 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.
- **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.
## Content IDs & namespacing
All registered content (blocks, items, recipes, biomes, entities, …) is identified by a **namespaced string** of the form `"namespace:id"`.
- **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.
- **Charset:** namespace and id are each `[a-z0-9_-]+`, exactly one `:` between them. Lowercase ASCII only. No uppercase, no Unicode, no spaces, no dots, no slashes. Keeps IDs greppable, filesystem-safe, and unambiguous in logs and save files.
- **Runtime representation:** intern each ID string into a small integer handle (e.g. `BlockId(u32)`) at registration time. Hot paths compare handles, not strings. Keep the original string for display, save/load, and the Lua API surface.
> *Project codename note:* "Catalyst" is the codename of the project, not the namespace. The engine namespace is deliberately `core:` so it stays stable if/when the project is renamed.
## Coordinate system & units
- **Up axis:** **+Y** (Minecraft-style).
- **Handedness:** **right-handed** (default math convention; +X right, +Y up, +Z toward the viewer / out of the screen).
- **World unit:** **1 unit = 1 block.** Blocks are simply smaller than Minecraft's (0.5 m physical scale), but inside the engine everything is counted in *blocks*, not metres. A player is therefore 3 units tall × 2 units wide in world coordinates.
Things to be aware of when writing rendering or import code (these are *not* convention changes — just gotchas you'll hit because the rest of the world disagrees):
- **Vulkan clip space is Y-down** by default (and Z is `[0, 1]`, not `[-1, 1]` like OpenGL). The projection matrix has to flip Y, or you set `viewport.height` negative — both are common idioms in `ash` examples. World/view space stays Y-up; only clip space differs.
- **Blender is Z-up, right-handed.** Models exported from Blender need a coordinate swap on import (rotate 90° around X, or swap Y/Z with sign). Decide once where that swap happens — at export, at import, or never (by adopting Blender's convention) — and stick to it. Doing it in two places will eventually produce a model that's mirrored or upside-down and you'll spend an afternoon on it.
- **glTF is Y-up, right-handed** — matches your engine convention, so it's the most friction-free model format if you have a choice.
## Commit conventions
[**Conventional Commits**](https://www.conventionalcommits.org/) with **mandatory crate-name scope**.
Format:
```
<type>(<crate>): <imperative subject>
[optional body]
[optional footer(s)]
```
- **Type** (required, exactly one): `feat` (new feature), `fix` (bug fix), `refactor` (no behaviour change), `perf`, `docs`, `test`, `chore` (build/tooling/deps), `build`, `ci`. Breaking changes append `!` before the colon: `feat(scripting)!: …`.
- **Scope** (required): the crate the change primarily affects — `client`, `server`, `renderer`, `shared`, `scripting`. For changes that genuinely span the whole workspace (e.g. workspace-level Cargo config, repo-wide `.gitattributes`), use `workspace`. For changes confined to non-Rust assets, use `assets`. Avoid omitting the scope, and avoid inventing per-commit scopes.
- **Subject:** imperative mood ("add", not "added" / "adds"), lowercase, no trailing period, ≤ ~72 chars.
- **Body** (optional): wrap at ~72 chars, explain *why* not *what*.
Examples:
```
feat(scripting): expose blocks.register to lua
fix(renderer): clamp swapchain extent to surface caps
refactor(shared): split network message types into submodule
chore(workspace): bump ash to 0.39
docs(assets): document texture-pack overlay layout
feat(server)!: change tick rate from 20 to 30 Hz
```
If a single commit truly touches multiple crates and can't be reasonably split, that's a signal to split it. Only fall back to `workspace` scope when the change is intrinsically workspace-wide.
**Do not add Claude (or any AI assistant) as a co-author on commits.** No `Co-Authored-By: Claude …` trailers, no "Generated with Claude Code" footers. Commits are authored by the human running the work.
## Common commands
```bash