diff --git a/CLAUDE.md b/CLAUDE.md index 6194b01..77e27a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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>` for low-contention shared state, `Arc>` 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: + +``` +(): + +[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