docs(workspace): split contributor guidance from agent rules
This commit is contained in:
parent
17517ec715
commit
7d7c1cfed4
328
AGENTS.md
328
AGENTS.md
|
|
@ -1,223 +1,109 @@
|
||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
Guidance for AI coding agents (and human contributors) working in this repository. This file is the single source of truth for architecture, conventions, and workflow. Tool-specific entry points (e.g. `CLAUDE.md`) import it rather than duplicating it.
|
**CRITICAL:** This file contains the engineering context for AI coding agents.
|
||||||
|
|
||||||
## Project goal
|
## 🚨 Absolute Rules (Never Break These) / Do Not
|
||||||
|
- **DO NOT** bypass the Lua scripting layer for gameplay logic.
|
||||||
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`).
|
- **DO NOT** use `unwrap()` or `expect()` outside `main` or tests.
|
||||||
|
- **DO NOT** use `println!`; use `tracing`.
|
||||||
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, so 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).
|
- **DO NOT** introduce non-deterministic logic into worldgen (no `thread_rng`, no `HashMap` iteration).
|
||||||
|
- **DO NOT** build parallel registration systems or duplicate registries.
|
||||||
## Documentation map
|
|
||||||
|
## 📖 What to Read
|
||||||
Documentation is layered by altitude; keep content at the layer it belongs to so no single file accretes everything.
|
- **Read `AGENTS.md` (this file)** first for every task.
|
||||||
|
- **Read `DEVELOPMENT.md`** before making any architectural or cross-cutting changes.
|
||||||
- **This file (`AGENTS.md`)**: cross-cutting engineering conventions and architecture invariants, i.e. the rules that apply regardless of which feature is being touched. There is a finite set of these, so this file should grow slowly. Subsystem-specific specs do **not** belong here.
|
- **Read subsystem docs (`docs/` and rustdoc)** only for the specific crates you are modifying.
|
||||||
- **[`docs/`](docs/) + Rust module docs (`//!`, `///`)**: per-subsystem technical implementation docs. How an individual system (meshing, networking, worldgen, ...) is built. Prefer module docs next to the code; promote to a `docs/<subsystem>.md` note when the design spans multiple files.
|
|
||||||
- **[`docs/adr/`](docs/adr/)**: Architecture Decision Records capturing the *why* behind significant, hard-to-reverse choices, one append-only file per decision. See [`docs/README.md`](docs/README.md) for the full scheme and [`docs/adr/0001-record-architecture-decisions.md`](docs/adr/0001-record-architecture-decisions.md) for the practice.
|
## ✅ Before Changing Code
|
||||||
|
- Does this require updates to documentation or ADRs (`docs/adr/`)?
|
||||||
The canonical game-*design* specification (intent, world rules, gameplay behaviour) is maintained separately and is not part of this repository; this repo documents how that design is implemented.
|
- Which crate does this belong to? (Maintain strict boundaries).
|
||||||
|
- Is this a new gameplay feature? If so, it must be exposed via the Lua API.
|
||||||
## Workspace layout
|
|
||||||
|
## 🛠️ Modification Priorities
|
||||||
Cargo workspace (resolver = "3", edition 2024) with six crates under `crates/`:
|
1. **Implement the feature in Lua** if possible, using existing APIs.
|
||||||
|
2. **Extend the Lua API** if it lacks the required capability.
|
||||||
- `client`: binary. Windowed application using `winit` 0.30 (`ApplicationHandler` pattern, `ControlFlow::Poll`). Also pulls in `image`. Player-facing app titled "Synvael"; handles input, windowing, and drives the renderer.
|
3. **Modify Rust internals** only as a last resort to support the Lua API.
|
||||||
- `server`: binary. Authoritative game simulation (voxel world, combat, players). Used both for dedicated multiplayer hosts and as the simulation backend for single-player.
|
Avoid bypassing the scripting layer entirely for gameplay features.
|
||||||
- `renderer`: library. Voxel/scene rendering on Vulkan via `ash`, decoupled from windowing so it can be driven by `client`.
|
|
||||||
- `shared`: library. Types and protocol shared between `client` and `server` (world/voxel data, network messages, combat primitives). Stays lean and dep-light; no `mlua`, no rendering, no engine internals.
|
## 🏗️ Code Style & Edits
|
||||||
- `scripting`: library. Lua modding API and bindings (owns the `mlua` dependency, `UserData` wrappers around `shared` types, API table registration, mod loader). Both `client` and `server` depend on it.
|
- Prefer modifying existing systems over creating new abstractions.
|
||||||
- `net`: library. QUIC transport, connection lifecycle, and wire framing for the client↔server protocol; owns the async runtime (`tokio`) and the `quinn`/`rustls` dependencies. Both `client` and `server` depend on it. See [ADR-0010](docs/adr/0010-net-crate-async-runtime.md).
|
- Avoid duplicate registries, parallel APIs, unnecessary traits, and premature generic abstractions.
|
||||||
|
- Keep changes local to the relevant module unless the architecture requires otherwise.
|
||||||
When adding code, keep the boundary tight: protocol/data types and game-rule primitives go in `shared`; Lua API surface and `mlua` integration in `scripting`; GPU/draw code in `renderer`; transport and connection code in `net` (protocol message *types* stay in `shared`); only input, windowing, and presentation glue live in `client`. Avoid growing `client` with simulation logic since it must work identically against either a local or remote `server`.
|
|
||||||
|
## ❓ When Unsure
|
||||||
## Modding API (Lua): dogfooded
|
If an implementation conflicts with these rules, **prefer preserving the architecture over minimizing code changes.**
|
||||||
|
|
||||||
The game exposes a Lua modding API, and **the base game itself is built on top of that same API** rather than treating it as a separate add-on layer. Built-in content (blocks, items, entities, recipes, etc.) is defined through the modding API so that mod authors can read the shipped code as reference for what's possible and how to do it.
|
## 📦 Architectural Dependency Rules
|
||||||
|
- `shared` stays lean and dependency-light (no `mlua`, no rendering).
|
||||||
This has hard implications when adding features:
|
- `scripting` depends on `shared`, but `shared` does **not** depend on `scripting`.
|
||||||
|
- `client` and `server` depend on `shared`, `scripting`, and `net`.
|
||||||
- Any new gameplay primitive (a new block type, item, entity, ability, ...) needs to be reachable through the Lua API, not just a Rust-only path. If you add a Rust-side concept without an API surface, you've broken the dogfooding contract.
|
- `client` depends on `renderer`, but `server` does not.
|
||||||
- Prefer extending the API and then *using* it from the engine over adding a parallel Rust-only entry point.
|
- **Do not** put simulation logic in `client`.
|
||||||
- Keep the API stable and discoverable, since mod authors will be reading it. Avoid leaking engine internals through it.
|
|
||||||
- The API and its bindings live in the **`scripting`** crate. It owns the `mlua` dependency, the API table registration, and the mod loader. Both `client` and `server` depend on it; `shared` does **not**, since `shared` stays the lean protocol/data layer.
|
## 🔍 File Location Hints
|
||||||
- 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.
|
- `/assets/scripts/`: Shipped base game Lua scripts.
|
||||||
- Prefer wrapper newtypes inside `scripting` over `impl UserData for SharedType` in `shared`, to avoid coupling the protocol crate to `mlua`.
|
- `/mods/`: In-repo example mods/test fixtures.
|
||||||
|
- `<user-data-dir>/mods/`: Player-installed mods (resolved at runtime).
|
||||||
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).
|
- `crates/client/`: Windowing, input, presentation.
|
||||||
|
- `crates/server/`: Authoritative simulation.
|
||||||
## Assets
|
- `crates/shared/`: Core types, network protocols.
|
||||||
|
- `crates/scripting/`: Lua API bindings.
|
||||||
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.
|
- `crates/renderer/`: Vulkan graphics.
|
||||||
|
- `crates/net/`: QUIC networking.
|
||||||
Assets are published openly under CC-BY-NC-SA 4.0 (see `LICENSE.md`). Binary assets (textures, models, sounds, compiled shaders) are tracked via **Git LFS**; keep `.gitattributes` up to date when adding a new binary file type. Lua scripts and JSON data live in plain Git (text files).
|
|
||||||
|
## ⚙️ Basic Verification Commands
|
||||||
## Script locations
|
- Check compilation: `cargo check -p <crate>`
|
||||||
|
- Lint code: `cargo clippy --all-targets --all-features -- -D warnings`
|
||||||
Three distinct locations, do not mix them:
|
- Format Rust: `cargo fmt --all -- --check`
|
||||||
|
- Format Lua: `stylua .` and `selene .`
|
||||||
- **`/assets/scripts/`**: the base game's own Lua, shipped with the binary. This is the dogfooded "first-party mod" the engine loads through the same API mod authors use. Mirror the structure modders will use (e.g. `scripts/blocks/`, `scripts/items/`, `scripts/entities/`) so it serves as a working reference.
|
- Run tests: `cargo test -p <crate>`
|
||||||
- **`/mods/`** (top-level): in-repo example mods or test fixtures. Kept out of `/assets/` because they're not engine-shipped content, and out of `crates/` because they're not Rust source.
|
|
||||||
- **`<user-data-dir>/mods/`**: player-installed mods, loaded at runtime only. Resolved via the `directories` / `dirs` crate (Linux: `~/.local/share/synvael/mods/`, with platform equivalents elsewhere). Never read from a hard-coded path.
|
## 🚀 Quick Start Task Checklist
|
||||||
|
- [ ] Review "What to Read" and "Before Changing Code".
|
||||||
## Data packs & resource packs
|
- [ ] Check modification priorities (Lua vs Rust).
|
||||||
|
- [ ] Make edits following code style guidelines.
|
||||||
Two distinct, orthogonal systems; keep them separate, do not collapse them into one "pack" concept. **Resource packs** are client-side asset overlays (textures, sounds, models, fonts, language files; no logic). **Data packs** are declarative content definitions (JSON/TOML/RON): blocks, items, recipes, loot tables, biomes, tags.
|
- [ ] Run basic verification commands.
|
||||||
|
- [ ] Commit using Conventional Commits.
|
||||||
The load-bearing rule: **do not build a parallel registration system.** The data-pack loader reads the declarative files and calls the same Lua API the engine and Lua mods use, giving one source of truth (`data/blocks/stone.json` → loader → `blocks.register{ ... }`). Each schema is a stable contract; version it deliberately. The decision is recorded in [ADR-0007](docs/adr/0007-declarative-content-via-modding-api.md).
|
|
||||||
|
---
|
||||||
Full subsystem detail (load order, repo and user-data layout, resolution semantics) lives in [`docs/packs.md`](docs/packs.md).
|
|
||||||
|
### Additional Subsystem Context
|
||||||
## Contributing workflow
|
|
||||||
|
**Concurrency & State**
|
||||||
Before committing a change, verify it against the actual repo state rather than assuming it is correct: read the files, inspect `git diff`, and run `cargo check` / `cargo clippy` / `cargo test` as appropriate. Then follow this loop for each change:
|
- **Multithreaded:** Prefer message-passing and per-thread ownership over shared mutable state. Avoid large `Mutex` wrappers.
|
||||||
|
- **Vulkan queues:** Not free-threaded.
|
||||||
1. **Verify** the change is actually present and correct in the working tree.
|
- **Lua VMs:** Not thread-safe. Treat each VM as owned by a single thread.
|
||||||
2. **Run the linter and formatter** to ensure no regressions or style issues were introduced: `cargo clippy --all-targets --all-features -- -D warnings`, `cargo fmt --all -- --check`, `selene .`, and `stylua .`.
|
|
||||||
3. **Ensure useful comments are present** before committing: function doc comments (`///`) and inline comments above non-obvious logic, following the documentation style below.
|
**Determinism**
|
||||||
4. **Create a focused git commit** following the commit conventions below.
|
- **Worldgen:** Strictly seed-deterministic. Use fixed RNG algorithms (`wyrand`, `xoshiro`). Never use `rand::thread_rng()`. Do not rely on `HashMap` iteration order (use `BTreeMap` or `IndexMap`).
|
||||||
|
- **Simulation:** Server-authoritative but not lockstep. Platform-specific math and floats are permitted outside of worldgen.
|
||||||
Keep commits scoped to a single concept. Do not batch multiple unrelated changes into one commit, and do not leave a verified change uncommitted before moving on to the next.
|
|
||||||
|
**Logging & Error Handling**
|
||||||
## Concurrency model
|
- **Logging:** Use `tracing` and spans (`#[tracing::instrument]`). No `println!`.
|
||||||
|
- **Libraries (`shared`, `renderer`, `scripting`):** Use `thiserror`.
|
||||||
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:
|
- **Binaries (`client`, `server`):** Use `anyhow`.
|
||||||
|
|
||||||
- Prefer message-passing (channels: `crossbeam-channel`, `flume`, or `std::sync::mpsc`) and per-thread ownership over shared mutable state.
|
**Testing Expectations**
|
||||||
- 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", which is how you accidentally serialise the whole engine.
|
- Test pure algorithmic logic, correctness traps (e.g. integer overflow, div_euclid), and determinism (worldgen).
|
||||||
- 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.
|
- I/O and GPU code are tested via integration/visual verification.
|
||||||
- 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.
|
- Ensure new tests run successfully and do not break existing ones.
|
||||||
- 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.
|
|
||||||
|
**Linting**
|
||||||
## Logging & error handling
|
- The workspace uses strict lints (including banning `unwrap`, `expect`, `print`).
|
||||||
|
- Prefer `#[expect(...)]` over `#[allow(...)]`. Suppress narrowly and justify non-obvious suppressions. Never suppress `correctness` lints.
|
||||||
- **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, which is 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.
|
**Documentation Style**
|
||||||
- **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 `?`.
|
- Formal, objective tone. No "we" or "you".
|
||||||
- **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.
|
- All public/internal struct fields need `///` docs.
|
||||||
|
- Functions require `# Errors`, `# Panics`, and `# Safety` sections in that order.
|
||||||
## Testing policy
|
|
||||||
|
**Branching Strategy**
|
||||||
Tests are prioritised by risk, not by a coverage percentage. Effort is directed where "compiles and appears correct" does not guarantee correctness. The following categories require accompanying unit tests, written in the same change that introduces or modifies the logic:
|
- **Development branch:** `dev`.
|
||||||
|
- **Releases branch:** `main`.
|
||||||
- **Pure, algorithmic logic** — values in, values out, no I/O, no GPU, no windowing: coordinate and index math, packing/unpacking, meshing math, and similar self-contained computation. These are cheap to test and their edges are easy to get subtly wrong.
|
- **Large features:** Feature branch off `dev` (e.g., `feat/new-worldgen`).
|
||||||
- **Correctness traps** — behaviour where a plausible implementation is silently wrong on an edge case: sign handling, off-by-one, integer overflow or truncation, bit-packing boundaries. As a canonical example, world-to-chunk conversion floors via `div_euclid` rather than truncating via `/`; a test on negative inputs pins that contract and prevents a regression to `/`.
|
|
||||||
- **Load-bearing invariants, especially determinism** — per the determinism stance below, worldgen is seed-deterministic and bit-for-bit reproducible. That contract cannot be verified by inspection and is guarded by tests (for example, generating a chunk twice from one seed and asserting equality). Determinism is guarded aggressively.
|
**General Coding Standards**
|
||||||
|
- **Content IDs:** Strict `"namespace:id"` format (e.g. `"core:stone"`). Interned to handles at runtime.
|
||||||
Subsystems that are I/O- or hardware-bound — the `renderer`/Vulkan GPU paths, `client` windowing and input, and top-level binary wiring — are validated through integration and manual/visual verification rather than unit tests, since their behaviour depends on a live device, window, or process rather than on pure logic. The appropriate mechanism differs; the expectation of verification does not.
|
- **Coordinate system:** +Y up, right-handed. 1 unit = 1 block (0.5m).
|
||||||
|
- **Paths:** Linux/Windows only. Use `std::path::Path` and `directories` crate. No hard-coded `/home`.
|
||||||
Unit tests live beside the code (`#[cfg(test)] mod tests`) and run with `cargo test -p <crate>`.
|
- **Commits:** Conventional Commits with crate scope (e.g., `feat(scripting): ...`).
|
||||||
|
|
||||||
## Lint suppressions
|
|
||||||
|
|
||||||
The workspace opts into strict linting: Clippy's `pedantic` group plus restriction lints that ban `unwrap`, `expect`, and `print` outside the permitted contexts (see `[workspace.lints]` in the root `Cargo.toml`). Suppressions are therefore expected at specific sites, and are governed by these rules:
|
|
||||||
|
|
||||||
- **Prefer `#[expect(...)]` over `#[allow(...)]`** for a localised suppression. An `#[expect]` becomes a warning (`unfulfilled_lint_expectations`) if the lint it names no longer fires, so an obsolete suppression surfaces and is removed instead of lingering silently. `#[allow]` never self-reports and accumulates into dead noise.
|
|
||||||
- **Suppress narrowly.** Name the exact lint(s), and attach the attribute to the smallest scope that covers the site — a statement, expression, or item — never a broad crate-level `#![allow]`. The sole standing exception is a deliberate crate-wide policy, such as `#![allow(unsafe_code)]` in `renderer`, where the suppression is the architectural intent rather than a local waiver.
|
|
||||||
- **Justify non-obvious suppressions.** Where the reason a lint is safe to suppress is not self-evident from the surrounding code, precede the attribute with a brief comment stating why (for example, that a cast is provably in range).
|
|
||||||
- **Never suppress `correctness`-tier lints.** Those indicate real defects; fix the code instead.
|
|
||||||
|
|
||||||
## Documentation style
|
|
||||||
|
|
||||||
- **Objective Tone:** All comments (both doc comments `///` and inline `//`) must be written in a formal, objective, and neutral tone.
|
|
||||||
- **No Personal Pronouns:** Avoid first-person ("we", "our", "us") or second-person ("you", "your") pronouns.
|
|
||||||
- **Voice:** Use the passive voice or neutral descriptive language. Instead of "We initialize the buffer," use "The buffer is initialized." Instead of "Your vertex shader needs this," use "The vertex shader requires this."
|
|
||||||
- **Focus:** Describe the code's behavior, the system's state, or technical invariants.
|
|
||||||
- **Struct Documentation:** Every field in a public or internal struct must have a doc comment (`///`) explaining its purpose and any invariants.
|
|
||||||
- **Function documentation sections:** Function doc comments follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/documentation.html) standard sections, in this fixed order after the summary and any extended description: `# Errors`, then `# Panics`, then `# Safety`. The sections apply to **all** functions, public and private (clippy only enforces the public ones; the same standard is expected on private helpers by hand).
|
|
||||||
- **`# Errors`** is mandatory on every function returning `Result`, and states the conditions under which each error variant is returned. `fn main` is exempt.
|
|
||||||
- **`# Panics`** is mandatory on any function that can panic (an `expect`/`unwrap`/`panic!`/`assert!`/indexing/arithmetic that can trip), and states the condition that triggers the panic.
|
|
||||||
- **`# Safety`** is mandatory on every `unsafe fn`, and states the invariants the caller must uphold.
|
|
||||||
- Test functions (`#[test]`, and helpers inside `#[cfg(test)]`) are exempt from all three; they are not part of the documented surface.
|
|
||||||
- Enforcement: `missing_errors_doc`, `missing_panics_doc`, and `missing_safety_doc` are warnings in the workspace lint set, so a missing section on a public item fails CI.
|
|
||||||
- **Stability:** Treat the documentation as a technical specification for the engine.
|
|
||||||
- **Line breaks:** Do not insert line returns inside a comment unless necessary. A comment that fits on a single line stays on a single line; do not pre-wrap at ~80 chars for aesthetics. Only break across lines when the comment is genuinely long (multi-sentence prose, enumerated invariants) or when a hard break carries meaning (separating an intro line from a bullet list, for instance).
|
|
||||||
|
|
||||||
## 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. 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.
|
|
||||||
- **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
|
|
||||||
|
|
||||||
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"`).
|
|
||||||
- **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 name note:* The project is named **Synvael** ("Catalyst" was the working codename). The engine namespace is deliberately `core:` (not the project name), so it stays stable independent of branding.
|
|
||||||
|
|
||||||
## Coordinate system & units
|
|
||||||
|
|
||||||
- **Up axis:** **+Y**.
|
|
||||||
- **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 0.5 m in 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.
|
|
||||||
|
|
||||||
Implementation gotchas that arise because neighbouring tools use different conventions (Vulkan clip space, Blender import, glTF) are collected in [`docs/rendering.md`](docs/rendering.md). They are not convention changes, only mismatches to handle in one agreed place.
|
|
||||||
|
|
||||||
## Branching Strategy & Workflow
|
|
||||||
|
|
||||||
- **`main` vs `dev`**: The repository follows a strict workflow. The `main` branch is reserved purely for stable releases. All active development happens on the **`dev`** branch.
|
|
||||||
- **Feature Branches**: For any large feature, always create a new branch off of `dev` (e.g., `feat/new-worldgen`). Do not commit large, work-in-progress features directly to `dev`. Once the feature is complete and verified, merge it back into `dev`.
|
|
||||||
- **Continuous Integration**: The project enforces strict linting and formatting via GitHub Actions. This includes workspace-level `clippy` rules (banning `unwrap` and `println!`), `cargo fmt`, `selene` for Lua, and `stylua`. Always ensure your code passes these tools locally before pushing.
|
|
||||||
|
|
||||||
## 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, one of `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:** keep commit messages short and simple, usually subject only, no body. The exception is `fix(...)` commits for non-trivial bugs, where a body explaining the root cause and why the fix works is valuable. Don't pad routine `feat`/`refactor`/`chore`/`docs` commits with bodies.
|
|
||||||
|
|
||||||
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 any AI assistant as a co-author on commits.** No `Co-Authored-By: ...` trailers for assistants, no "Generated with ..." footers. Commits are authored by the human running the work.
|
|
||||||
|
|
||||||
## Common commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build # build all crates
|
|
||||||
cargo run -p client # run the windowed client
|
|
||||||
cargo run -p server # run the server
|
|
||||||
cargo test # run all tests
|
|
||||||
cargo test -p renderer it_works # run a single test by name
|
|
||||||
cargo check -p <crate> # fast type-check one crate
|
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
|
||||||
cargo fmt
|
|
||||||
selene .
|
|
||||||
stylua .
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Project guidance for this repository is maintained in **[AGENTS.md](AGENTS.md)**, the tool-agnostic single source of truth for architecture, conventions, and workflow. It is imported below so Claude Code loads it directly; edit `AGENTS.md`, not this file.
|
See `AGENTS.md` for project instructions. Do not edit this file.
|
||||||
|
|
||||||
@AGENTS.md
|
@AGENTS.md
|
||||||
|
|
|
||||||
151
CONTRIBUTING.md
151
CONTRIBUTING.md
|
|
@ -1,167 +1,132 @@
|
||||||
# Contributing to Synvael
|
# Contributing to Synvael
|
||||||
|
|
||||||
Thank you for your interest in contributing to Synvael. This document explains how contributions work, what is expected, and what contributors receive in return.
|
Thanks for your interest in contributing to Synvael. This guide covers how contributions work, our expectations, and what you get out of it as a contributor.
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
Synvael is **open-source and commercially developed**. The engine source code is licensed under AGPLv3, official game assets under CC-BY-NC-SA 4.0 (see [LICENSE.md](LICENSE.md) for full terms), and the project is built by Cryoforge Nexus, a commercial entity that intends to monetize the finished game.
|
Synvael is **open-source and commercially developed**. The engine code uses the AGPLv3 license, and official game assets use CC-BY-NC-SA 4.0 (check [LICENSE.md](LICENSE.md) for the details). The project is built by Cryoforge Nexus, a commercial company that plans to monetize the finished game.
|
||||||
|
|
||||||
Outside contributions are welcome and credited, but they do not carry equity or revenue share. If that trade does not work for you, please do not contribute. The project would rather be upfront about these terms than have anyone feel misled later.
|
We welcome and credit outside contributions, but they don't include equity or revenue share. If that arrangement doesn't work for you, it's best not to contribute. We prefer being upfront about this rather than risking anyone feeling misled later on.
|
||||||
|
|
||||||
## Contributor Tiers
|
## Contributor Tiers
|
||||||
|
|
||||||
### Core team
|
### Core team
|
||||||
|
|
||||||
The core team makes architectural and roadmap decisions and is eligible for an ownership stake in Cryoforge Nexus, subject to the company's internal agreements.
|
The core team handles architectural and roadmap decisions. They are eligible for an ownership stake in Cryoforge Nexus based on the company's internal agreements.
|
||||||
|
|
||||||
Membership is not open by default, but there is a path in. A contributor becomes **eligible for consideration** when all of the following are met:
|
Membership isn't open by default, but there is a way in. A contributor becomes **eligible for consideration** when they meet all these criteria:
|
||||||
|
|
||||||
1. **Sustained contribution.** At least six months of active, merged contributions. Consistency and quality matter more than volume.
|
1. **Sustained contribution:** At least six months of active, merged contributions. Consistency and quality are more important than sheer volume.
|
||||||
2. **Architectural ownership.** The contributor has taken responsibility for at least one subsystem (meshing, networking, worldgen, a major Lua API surface, etc.) and has demonstrated the ability to make sound design decisions within it, not just implement assigned tasks.
|
2. **Architectural ownership:** The contributor has taken charge of at least one subsystem (like meshing, networking, worldgen, or a major Lua API surface) and has shown they can make good design decisions there, rather than just completing assigned tasks.
|
||||||
3. **Community trust.** A track record of constructive code reviews, issue discussions, and collaboration. No pattern of conflict or disregard for project norms.
|
3. **Community trust:** A solid track record of constructive code reviews, issue discussions, and teamwork. No history of conflict or ignoring project norms.
|
||||||
|
|
||||||
When these criteria are met, an existing core member may **nominate** the contributor. Admission requires a **supermajority vote** (two-thirds or more) of current core members. The nominee may decline.
|
Once these criteria are met, an existing core member can **nominate** the contributor. Joining requires a **supermajority vote** (two-thirds or more) from the current core members, and the nominee is free to decline.
|
||||||
|
|
||||||
Meeting the criteria makes someone eligible for consideration, not entitled to membership. The final decision remains a human judgement about long-term fit with the team.
|
Meeting the criteria just makes someone eligible, not entitled to membership. The final call is a human judgement about long-term fit with the team.
|
||||||
|
|
||||||
### Outside contributors
|
### Outside contributors
|
||||||
|
|
||||||
Anyone who submits a pull request, asset, bug report, translation, or documentation change. Outside contributors receive public credit, authorship on their work under the project's open-source licenses, code review, mentorship where useful, and a voice in technical discussion. They do not receive equity, revenue share, or a guarantee of future paid work.
|
This includes anyone who submits a pull request, asset, bug report, translation, or documentation change. Outside contributors get public credit, authorship on their work under our open-source licenses, code reviews, mentorship when helpful, and a voice in technical discussions. They do not get equity, revenue share, or any guarantee of future paid work.
|
||||||
|
|
||||||
## Contributor License Agreement (CLA)
|
## Contributor License Agreement (CLA)
|
||||||
|
|
||||||
Every contribution, code and assets alike, requires signing a [Contributor License Agreement](CLA.md) before a pull request can be merged. The CLA does **not** transfer copyright, contributors retain it. It grants Cryoforge Nexus a perpetual, irrevocable license to use, modify, sublicense, and relicense the contribution, including under commercial terms.
|
Every contribution (both code and assets) requires signing a [Contributor License Agreement](CLA.md) before we can merge a pull request. The CLA does **not** transfer your copyright; you keep it. It simply grants Cryoforge Nexus a perpetual, irrevocable license to use, modify, sublicense, and relicense your contribution, including for commercial purposes.
|
||||||
|
|
||||||
This is necessary because the public licenses (AGPLv3 + CC-BY-NC-SA 4.0) would otherwise prevent the planned monetization model. Without the CLA, contributed assets in particular would lock the project out of any commercial release.
|
This is necessary because the public licenses (AGPLv3 and CC-BY-NC-SA 4.0) would otherwise block our monetization plan. Without the CLA, contributed assets in particular would prevent the project from having a commercial release.
|
||||||
|
|
||||||
### How to sign
|
### How to sign
|
||||||
|
|
||||||
When a pull request is opened, an automated check verifies whether all commit authors have signed the CLA. If not, it posts a comment with instructions. To sign, leave a comment on the pull request containing:
|
When you open a pull request, an automated check looks to see if all commit authors have signed the CLA. If not, it drops a comment with instructions. To sign, just leave a comment on the pull request saying:
|
||||||
|
|
||||||
```
|
```
|
||||||
I have read the CLA and I agree
|
I have read the CLA and I agree
|
||||||
```
|
```
|
||||||
|
|
||||||
The bot records the signature and updates the check status automatically. Signing is a one-time action; once recorded, all future pull requests from the same GitHub account are accepted without re-signing.
|
The bot records your signature and updates the check status automatically. You only have to do this once. After that, all future pull requests from your GitHub account are accepted without needing to sign again.
|
||||||
|
|
||||||
## What is accepted
|
## What is accepted
|
||||||
|
|
||||||
- **Code** in Rust (for engine crates) or Lua (for `assets/scripts/` and `mods/`), licensed under AGPLv3.
|
- **Code** in Rust (for engine crates) or Lua (for `assets/scripts/` and `mods/`), licensed under AGPLv3.
|
||||||
- **Original assets** authored by the contributor: textures, models, sounds, icons, shaders. Licensed under CC-BY-NC-SA 4.0.
|
- **Original assets** you created yourself: textures, models, sounds, icons, shaders. Licensed under CC-BY-NC-SA 4.0.
|
||||||
- **Translations, documentation, bug reports, and design feedback.**
|
- **Translations, documentation, bug reports, and design feedback.**
|
||||||
|
|
||||||
## What is not accepted
|
## What is not accepted
|
||||||
|
|
||||||
- **Assets derived from copyrighted third-party material** (other games, films, copyrighted art). All submitted assets must be original work.
|
- **Assets derived from copyrighted third-party material** (like other games, films, or copyrighted art). Everything submitted must be your original work.
|
||||||
- **AI-generated assets.** Textures, models, sounds, icons, and other non-code assets must be original human-authored work. See [AI-Assisted Contributions](#ai-assisted-contributions) below.
|
- **AI-generated assets.** Textures, models, sounds, icons, and any other non-code assets must be originally made by humans. Check the [AI-Assisted Contributions](#ai-assisted-contributions) section below.
|
||||||
- **Contributions without a signed CLA.** The bot enforces this; unsigned pull requests cannot be merged.
|
- **Contributions without a signed CLA.** The bot enforces this strictly, so unsigned pull requests won't be merged.
|
||||||
- **Native (Rust) mods submitted as pull requests.** Native mods that link against engine internals are derivative works under AGPLv3 and belong in their own repositories. Lua mods are welcome in `mods/`.
|
- **Native (Rust) mods submitted as pull requests.** Native mods that link against engine internals are considered derivative works under AGPLv3 and belong in their own separate repositories. Lua mods are completely welcome in `mods/`.
|
||||||
|
|
||||||
## Development Setup
|
## Coding Standards & Architecture
|
||||||
|
|
||||||
### Prerequisites
|
For the complete technical engineering manual, including architecture boundaries, naming, documentation style, and lint rules, please refer to [DEVELOPMENT.md](DEVELOPMENT.md).
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org/tools/install) (stable toolchain, edition 2024)
|
## Pull Request Process
|
||||||
- [Git LFS](https://git-lfs.com/) (binary assets are tracked via LFS)
|
|
||||||
- A Vulkan-capable GPU with up-to-date drivers (Linux or Windows)
|
|
||||||
|
|
||||||
### Building
|
1. **Branch from `dev`.** The `main` branch is kept strictly for stable releases. All active development happens on the **`dev`** branch. If you are working on a large feature, always create a new feature branch off `dev` (for example, `feat/new-worldgen`). Don't commit large, work-in-progress features directly to `dev`.
|
||||||
|
2. **One concept per pull request.** Try to keep your changes focused. If a pull request touches multiple unrelated systems, please split it up.
|
||||||
|
3. **Ensure CI passes.** Our pipeline runs `cargo fmt`, `cargo clippy`, `selene`, and `stylua`. Pull requests with lint failures won't be reviewed. Always make sure your code passes these tools locally before pushing.
|
||||||
|
4. **Sign the CLA.** The CLA bot must show a passing status before we start reviewing.
|
||||||
|
5. **Describe the change.** Explain what your pull request does, why it's needed, and any design decisions you made. If it relates to any open issues, link them.
|
||||||
|
6. **Respond to review feedback.** Maintainers might request changes. Please address them or discuss alternative approaches with us.
|
||||||
|
|
||||||
```bash
|
## Contributing workflow
|
||||||
git clone https://github.com/Cryoforge-Nexus/Synvael.git
|
|
||||||
cd Synvael
|
|
||||||
git lfs pull
|
|
||||||
cargo build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running
|
Before you commit a change, verify it against the actual repo state instead of assuming it's correct. Read the files, inspect `git diff`, and run `cargo check`, `cargo clippy`, or `cargo test` as needed. Then just follow this loop for each change:
|
||||||
|
|
||||||
```bash
|
1. **Verify** that the change is actually present and correct in your working tree.
|
||||||
cargo run -p client # windowed client
|
2. **Run the linter and formatter** to ensure no regressions or style issues sneaked in: `cargo clippy --all-targets --all-features -- -D warnings`, `cargo fmt --all -- --check`, `selene .`, and `stylua .`.
|
||||||
cargo run -p server # dedicated server
|
3. **Ensure useful comments are present** before committing: add function doc comments (`///`) and inline comments above non-obvious logic, sticking to the project's documentation style.
|
||||||
```
|
4. **Create a focused git commit** using the commit conventions listed below.
|
||||||
|
|
||||||
### Testing
|
Keep commits scoped to a single concept. Don't bundle multiple unrelated changes into one commit, and try not to leave a verified change uncommitted before moving on to the next thing.
|
||||||
|
|
||||||
```bash
|
## Commit conventions
|
||||||
cargo test # all tests
|
|
||||||
cargo test -p shared # tests for a single crate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linting
|
We use [**Conventional Commits**](https://www.conventionalcommits.org/) with a **mandatory crate-name scope**.
|
||||||
|
|
||||||
The CI pipeline enforces strict linting. Run these locally before pushing:
|
Format:
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo fmt --all -- --check
|
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
|
||||||
selene .
|
|
||||||
stylua .
|
|
||||||
```
|
|
||||||
|
|
||||||
Lua linting requires [Selene](https://kampfkarren.github.io/selene/) and [StyLua](https://github.com/JohnnyMorganz/StyLua). Install them via `cargo install selene` and `cargo install stylua`, or use the pre-built binaries from their release pages.
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
All conventions, commit format, documentation style, naming, architecture boundaries and lint rules are documented in [AGENTS.md](AGENTS.md). That file is the single source of truth; this section highlights the most relevant points for contributors.
|
|
||||||
|
|
||||||
### Commit format
|
|
||||||
|
|
||||||
[Conventional Commits](https://www.conventionalcommits.org/) with a mandatory crate-name scope:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>(<crate>): <imperative subject>
|
<type>(<crate>): <imperative subject>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
```
|
```
|
||||||
|
|
||||||
Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`. Scope is the primary crate affected (`client`, `server`, `renderer`, `shared`, `scripting`) or `workspace` / `assets` for cross-cutting changes.
|
- **Type** (required, exactly one): `feat` (new feature), `fix` (bug fix), `refactor` (no behaviour change), `perf`, `docs`, `test`, `chore` (build/tooling/deps), `build`, `ci`. If there are breaking changes, append a `!` before the colon: `feat(scripting)!: ...`.
|
||||||
|
- **Scope** (required): the crate the change primarily affects, choosing from `client`, `server`, `renderer`, `shared`, or `scripting`. If the change spans the whole workspace (like a Cargo config change or `.gitattributes`), use `workspace`. For changes isolated to non-Rust assets, use `assets`. Please avoid omitting the scope and don't make up new scopes per commit.
|
||||||
|
- **Subject:** imperative mood (use "add", not "added" or "adds"), lowercase, no trailing period, and keep it under 72 characters.
|
||||||
|
- **Body:** keep commit messages short and sweet. Usually just a subject is fine. The main exception is `fix(...)` commits for non-trivial bugs, where a body explaining the root cause and why the fix actually works is super helpful. There's no need to pad routine commits with bodies.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(scripting): expose blocks.register to lua
|
feat(scripting): expose blocks.register to lua
|
||||||
fix(renderer): clamp swapchain extent to surface caps
|
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
|
docs(assets): document texture-pack overlay layout
|
||||||
|
feat(server)!: change tick rate from 20 to 30 Hz
|
||||||
```
|
```
|
||||||
|
|
||||||
### Documentation
|
If a single commit touches multiple crates and honestly can't be reasonably split, that's usually a sign that it should be split anyway. Only fall back to the `workspace` scope when the change is fundamentally workspace-wide.
|
||||||
|
|
||||||
- Formal, objective tone. No first-person or second-person pronouns.
|
|
||||||
- Every public struct field requires a `///` doc comment.
|
|
||||||
- Inline comments (`//`) above non-obvious logic.
|
|
||||||
|
|
||||||
### Architecture boundaries
|
|
||||||
|
|
||||||
- Protocol/data types → `shared`
|
|
||||||
- Lua API and `mlua` integration → `scripting`
|
|
||||||
- GPU/draw code → `renderer`
|
|
||||||
- Input, windowing, presentation → `client`
|
|
||||||
- Simulation, authoritative logic → `server`
|
|
||||||
|
|
||||||
Do not place simulation logic in `client`. See [AGENTS.md](AGENTS.md) for the full rationale.
|
|
||||||
|
|
||||||
## Pull Request Process
|
|
||||||
|
|
||||||
1. **Branch from `dev`.** The `main` branch is reserved for stable releases. All development happens on `dev`. For any feature of non-trivial scope, create a feature branch off `dev` (e.g., `feat/new-worldgen`).
|
|
||||||
2. **One concept per pull request.** Keep changes focused. If a pull request touches multiple unrelated systems, split it.
|
|
||||||
3. **Ensure CI passes.** The pipeline runs `cargo fmt`, `cargo clippy`, `selene`, and `stylua`. Pull requests with lint failures are not reviewed.
|
|
||||||
4. **Sign the CLA.** The CLA bot must report a passing status before review begins.
|
|
||||||
5. **Describe the change.** Explain what the pull request does, why it is needed, and any design decisions made. Link to relevant issues if applicable.
|
|
||||||
6. **Respond to review feedback.** Maintainers may request changes. Please address them or discuss alternatives.
|
|
||||||
|
|
||||||
## Recognition
|
## Recognition
|
||||||
|
|
||||||
Every merged contribution earns an entry in `CREDITS.md`. Substantial or sustained contributions are highlighted on the project website. Standout contributors may, once the project has revenue, be offered paid bounties for specific scoped work, this is a transactional arrangement, not equity or an ongoing revenue share.
|
Every merged contribution gets an entry in `CREDITS.md`. Major or sustained contributions are highlighted on the project website. Standout contributors might be offered paid bounties for specific scoped work once the project starts generating revenue. This is a transactional setup, not equity or an ongoing revenue share.
|
||||||
|
|
||||||
This is the honest ceiling of what outside contribution earns. If the goal is co-ownership of a game studio, this project is not the right fit.
|
This is the hard limit of what outside contribution earns. If your goal is co-ownership of a game studio, this project probably isn't the right fit.
|
||||||
|
|
||||||
## AI-Assisted Contributions
|
## AI-Assisted Contributions
|
||||||
|
|
||||||
AI tools (code completion, generation, refactoring assistants) may be used as aids when writing code. The following rules apply:
|
You are welcome to use AI tools like code completion, generation, or refactoring assistants when writing code. Just keep these rules in mind:
|
||||||
|
|
||||||
- **AI-assisted code is accepted, with conditions.** The contributor must have genuinely reviewed every line of the submitted code and be able to explain what it does and why. Contributions where the author cannot answer questions about their own code during review will be rejected. The contributor, not the AI tool, is the responsible author.
|
- **AI-assisted code is accepted, with conditions.** You must genuinely review every line of the submitted code and be able to explain what it does and why. If you can't answer questions about your own code during review, the contribution will be rejected. You are the responsible author, not the AI tool.
|
||||||
- **AI-generated assets are not accepted.** Textures, models, sounds, icons, and other non-code assets must be original human-authored work. This applies regardless of the AI tool or its training-data provenance.
|
- **AI-generated assets are not accepted.** Textures, models, sounds, icons, and other non-code assets must be original human-authored work. This rule applies regardless of the tool used or where its training data came from.
|
||||||
- **Disclosure is required.** If AI tools were used in a material way during the creation of a code contribution, this must be stated in the pull request description. A brief note (e.g., "AI-assisted: used Copilot for boilerplate generation") is sufficient.
|
- **Disclosure is required.** If you used AI tools in a meaningful way to create a code contribution, state this in the pull request description. A quick note like "AI-assisted: used Copilot for boilerplate generation" is completely fine.
|
||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
Open an issue or start a discussion on the repository. Maintainers are happy to help with setup, answer questions about the architecture, or discuss proposed changes before work begins.
|
Feel free to open an issue or start a discussion on the repository. Maintainers are always happy to help with setup, answer questions about the architecture, or chat about proposed changes before you start working on them.
|
||||||
|
|
|
||||||
195
DEVELOPMENT.md
Normal file
195
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
# Development Guidelines
|
||||||
|
|
||||||
|
This file is the single source of truth for architecture, conventions, and workflow for human contributors working on Synvael. Tool-specific entry points (like `CLAUDE.md`) and AI instructions (like `AGENTS.md`) import or summarize this file rather than duplicating it.
|
||||||
|
|
||||||
|
## Documentation map
|
||||||
|
|
||||||
|
Our documentation is layered by altitude. Try to keep content at the layer it belongs to so no single file ends up hoarding everything.
|
||||||
|
|
||||||
|
- **This file (`DEVELOPMENT.md`)**: covers cross-cutting engineering conventions and architecture invariants. These are the rules that apply no matter what feature you're touching. There is a finite set of these, so this file should grow pretty slowly. Subsystem-specific specs do not belong here.
|
||||||
|
- **[`docs/`](docs/) and Rust module docs (`//!`, `///`)**: per-subsystem technical implementation docs explaining how an individual system (meshing, networking, worldgen, etc.) is actually built. We prefer module docs right next to the code. If a design spans multiple files, promote it to a `docs/<subsystem>.md` note.
|
||||||
|
- **[`docs/adr/`](docs/adr/)**: Architecture Decision Records capturing the "why" behind significant, hard-to-reverse choices, with one append-only file per decision. Check out [`docs/README.md`](docs/README.md) for the full structure and [`docs/adr/0001-record-architecture-decisions.md`](docs/adr/0001-record-architecture-decisions.md) for the practice itself.
|
||||||
|
|
||||||
|
The canonical game design specification covering intent, world rules, and gameplay behaviour is maintained separately and isn't part of this repository. This repo only documents how that design gets implemented.
|
||||||
|
|
||||||
|
## Workspace layout
|
||||||
|
|
||||||
|
We use a Cargo workspace (resolver = "3", edition 2024) containing six crates under `crates/`:
|
||||||
|
|
||||||
|
- `client`: binary. This is the windowed application using `winit` 0.30 (`ApplicationHandler` pattern, `ControlFlow::Poll`). It also pulls in `image`. This is the player-facing app titled "Synvael". It handles input, windowing, and drives the renderer.
|
||||||
|
- `server`: binary. The authoritative game simulation covering the voxel world, combat, and players. It is used for dedicated multiplayer hosts and also acts as the simulation backend for single-player.
|
||||||
|
- `renderer`: library. Voxel and scene rendering on Vulkan using `ash`. It is deliberately decoupled from windowing so the `client` can drive it.
|
||||||
|
- `shared`: library. Types and protocols shared between `client` and `server` like world and voxel data, network messages, and combat primitives. This crate stays lean and dependency-light. It has no `mlua`, no rendering, and no engine internals.
|
||||||
|
- `scripting`: library. The Lua modding API and bindings. This crate owns the `mlua` dependency, `UserData` wrappers around `shared` types, API table registration, and the mod loader. Both `client` and `server` depend on it.
|
||||||
|
- `net`: library. QUIC transport, connection lifecycle, and wire framing for the client-server protocol. It owns the async runtime (`tokio`) and the `quinn` and `rustls` dependencies. Both `client` and `server` depend on it. See [ADR-0010](docs/adr/0010-net-crate-async-runtime.md) for more details.
|
||||||
|
|
||||||
|
When adding code, please keep these boundaries tight. Protocol and data types plus game-rule primitives go in `shared`. Lua API surfaces and `mlua` integration live in `scripting`. GPU and drawing code goes in `renderer`. Transport and connection code belongs in `net` (but protocol message types stay in `shared`). Only input, windowing, and presentation glue should live in `client`. Try to avoid growing `client` with simulation logic, since it needs to work identically whether it's talking to a local or remote `server`.
|
||||||
|
|
||||||
|
## Modding API (Lua): dogfooded
|
||||||
|
|
||||||
|
The game exposes a Lua modding API, and **the base game itself is built directly on top of that same API** rather than treating it as a separate add-on layer. Built-in content like blocks, items, entities, and recipes are defined through the modding API so mod authors can read the shipped code as a reference for what's possible and how to do it.
|
||||||
|
|
||||||
|
This has some strict implications when adding new features:
|
||||||
|
|
||||||
|
- Any new gameplay primitive (a new block type, item, entity, ability, etc.) needs to be accessible through the Lua API, not just as a Rust-only path. If you add a Rust-side concept without an API surface, you've broken our dogfooding rule.
|
||||||
|
- Prefer extending the API and then *using* it from the engine over adding a parallel Rust-only entry point.
|
||||||
|
- Keep the API stable and easy to discover, since mod authors will be reading it. Avoid leaking engine internals through it.
|
||||||
|
- The API and its bindings live strictly in the **`scripting`** crate. It owns the `mlua` dependency, the API table registration, and the mod loader. Both `client` and `server` depend on it. `shared` does **not**, since it needs to stay as a lean protocol layer.
|
||||||
|
- Authoritative APIs like world mutation and combat resolution are defined in `scripting` but gated so the client-side Lua VM cannot invoke them. We use one API surface across two execution contexts: the client VM is for read-only UI and effects, while the server VM is authoritative.
|
||||||
|
- Use wrapper newtypes inside `scripting` rather than `impl UserData for SharedType` in `shared`. This prevents 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 are recorded in [ADR-0006](docs/adr/0006-base-game-on-modding-api.md).
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
All game assets live under `/assets` at the repo root, organized into subfolders by kind: `icons/`, `models/`, `shaders/`, `sounds/`, `textures/`, and `scripts/`. New assets must go in the matching subfolder. Don't drop loose files directly into `/assets`, and don't scatter assets inside crate directories.
|
||||||
|
|
||||||
|
Assets are published openly under CC-BY-NC-SA 4.0 (check `LICENSE.md`). Binary assets like textures, models, sounds, and compiled shaders are tracked using **Git LFS**. Keep `.gitattributes` up to date when adding a new binary file type. Lua scripts and JSON data are plain text files and live in standard Git.
|
||||||
|
|
||||||
|
## Script locations
|
||||||
|
|
||||||
|
We use three distinct locations for scripts. Please do not mix them:
|
||||||
|
|
||||||
|
- **`/assets/scripts/`**: the base game's own Lua, shipped with the binary. This is the dogfooded "first-party mod" the engine loads through the same API mod authors use. We mirror the structure modders will use (like `scripts/blocks/`, `scripts/items/`, `scripts/entities/`) so it serves as a working reference.
|
||||||
|
- **`/mods/`** (top-level): in-repo example mods or test fixtures. We keep these out of `/assets/` because they aren't engine-shipped content, and out of `crates/` because they aren't Rust source code.
|
||||||
|
- **`<user-data-dir>/mods/`**: player-installed mods, loaded only at runtime. This path is resolved via the `directories` or `dirs` crate (on Linux, it's `~/.local/share/synvael/mods/`, with platform equivalents elsewhere). Never read from a hard-coded path.
|
||||||
|
|
||||||
|
## Data packs & resource packs
|
||||||
|
|
||||||
|
These are two distinct, orthogonal systems. Keep them separate, and don't merge them into one "pack" concept. **Resource packs** are client-side asset overlays covering textures, sounds, models, fonts, and language files. They contain no logic. **Data packs** are declarative content definitions (using JSON, TOML, or RON) covering blocks, items, recipes, loot tables, biomes, and tags.
|
||||||
|
|
||||||
|
Our strict rule here: **do not build a parallel registration system.** The data-pack loader reads declarative files and calls the exact same Lua API that the engine and Lua mods use, ensuring one single source of truth (e.g. `data/blocks/stone.json` is read by the loader, which calls `blocks.register{ ... }`). Each schema is a stable contract that we version deliberately. This decision is recorded in [ADR-0007](docs/adr/0007-declarative-content-via-modding-api.md).
|
||||||
|
|
||||||
|
Full subsystem details regarding load order, repo and user-data layouts, and resolution semantics can be found in [`docs/packs.md`](docs/packs.md).
|
||||||
|
|
||||||
|
## Concurrency model
|
||||||
|
|
||||||
|
The game is **multithreaded by design**. A single-threaded approach simply wouldn't meet our performance budget for running voxel meshing, worldgen, rendering, networking, and simulation all at once. Assume multiple threads when writing code and design data ownership accordingly:
|
||||||
|
|
||||||
|
- Prefer message-passing using channels (`crossbeam-channel`, `flume`, or `std::sync::mpsc`) and per-thread ownership rather than shared mutable state.
|
||||||
|
- When sharing is completely unavoidable, use the right primitive for your access pattern. Use `Arc<Mutex<_>>` for low-contention shared state, `Arc<RwLock<_>>` for read-heavy state, atomics like `AtomicU32` or `AtomicBool` for counters and flags, and lock-free structures from `crossbeam` or `dashmap` for hot paths. Try to avoid wrapping large hot data in a single `Mutex` "just in case", as this can easily accidentally serialize the entire engine.
|
||||||
|
- Worldgen and chunk meshing are massive parallelism wins. We expect a thread pool like `rayon` or a hand-rolled one to feed meshing and generation jobs.
|
||||||
|
- Vulkan command-buffer recording can also be parallelized, but Vulkan **queues** are not free-threaded. Only one thread can submit to a given queue at a time, so plan ownership of `vk::Queue` accordingly.
|
||||||
|
- The Lua VMs (one per execution context for client and server) are **not** thread-safe in `mlua`'s default configuration. Treat each VM as owned by a single thread, and dispatch work to and from it using channels.
|
||||||
|
|
||||||
|
## Logging & error handling
|
||||||
|
|
||||||
|
- **Logging:** We use [`tracing`](https://docs.rs/tracing/) with `tracing-subscriber` as the output backend. Use `info!`, `warn!`, `error!`, `debug!`, and `trace!` macros at appropriate levels. It's crucial to use **spans** (`#[tracing::instrument]`, `info_span!`) to scope work, as this is how we keep multithreaded log output readable. Avoid using `println!` or `eprintln!` for diagnostics. If it's worth printing, it's worth a proper `tracing` event.
|
||||||
|
- **Errors in libraries** (`shared`, `renderer`, `scripting`): Use typed error enums via [`thiserror`](https://docs.rs/thiserror/) using `#[derive(Error)]`. Each variant should be a distinct, matchable failure mode. Do not expose `anyhow::Error` from a library API.
|
||||||
|
- **Errors in binaries** (`client`, `server`): Use [`anyhow`](https://docs.rs/anyhow/) at the top level, leaning on `.context("...")` to provide human-readable layers. Library errors compose smoothly into `anyhow::Error` using the `?` operator.
|
||||||
|
- **Never use `.unwrap()` or `.expect()` outside of `main`, setup logic, or tests.** The only exception is when an invariant is genuinely impossible to violate. On hot paths, propagate errors with `?` and let the caller decide what to do.
|
||||||
|
|
||||||
|
## Testing policy
|
||||||
|
|
||||||
|
We prioritize tests based on risk, not raw coverage percentages. We direct our testing effort toward areas where code that compiles and appears correct isn't guaranteed to actually be correct. You must write accompanying unit tests for these categories in the same change that introduces or modifies the logic:
|
||||||
|
|
||||||
|
- **Pure algorithmic logic.** Things with values in, values out, no I/O, no GPU, and no windowing. This includes coordinate and index math, packing and unpacking, meshing math, and similar self-contained computations. These are cheap to test and their edges are notoriously easy to get subtly wrong.
|
||||||
|
- **Correctness traps.** Behaviors where a totally plausible implementation is silently wrong on an edge case. Examples include sign handling, off-by-one errors, integer overflow or truncation, and bit-packing boundaries. As a classic example, world-to-chunk conversion needs to floor via `div_euclid` rather than truncating via `/`. A test on negative inputs locks in that contract and prevents someone from accidentally regressing to `/`.
|
||||||
|
- **Load-bearing invariants (especially determinism).** As noted in our determinism stance below, worldgen is seed-deterministic and bit-for-bit reproducible. That contract can't be verified just by looking at the code, so it is strictly guarded by tests (for example, generating a chunk twice from one seed and asserting they are equal). We guard determinism aggressively.
|
||||||
|
|
||||||
|
Subsystems that are bound by I/O or hardware (like the `renderer` and Vulkan GPU paths, `client` windowing and input, and top-level binary wiring) are validated through integration tests and manual visual verification rather than strict unit tests. Their behavior relies on a live device, window, or process rather than pure logic. While the mechanism differs, the expectation that they are properly verified does not.
|
||||||
|
|
||||||
|
Unit tests live right next to the code as `#[cfg(test)] mod tests` and are run using `cargo test -p <crate>`.
|
||||||
|
|
||||||
|
## Lint suppressions
|
||||||
|
|
||||||
|
The workspace opts into a strict set of lints. This includes Clippy's `pedantic` group along with restriction lints that ban `unwrap`, `expect`, and `print` outside permitted contexts (you can check `[workspace.lints]` in the root `Cargo.toml`). Suppressions are expected at specific sites and are governed by these rules:
|
||||||
|
|
||||||
|
- **Always prefer `#[expect(...)]` over `#[allow(...)]`** for a localized suppression. An `#[expect]` turns into a warning (`unfulfilled_lint_expectations`) if the lint it targets no longer fires, meaning obsolete suppressions surface automatically and can be cleaned up instead of lingering silently. `#[allow]` never self-reports and just accumulates as dead noise.
|
||||||
|
- **Suppress narrowly.** Name the exact lint or lints, and attach the attribute to the absolute smallest scope that covers the site (like a statement, expression, or item). Never use a broad crate-level `#![allow]`. The only exception is a deliberate crate-wide policy, such as `#![allow(unsafe_code)]` in the `renderer`, where the suppression represents an architectural intent rather than a local waiver.
|
||||||
|
- **Justify non-obvious suppressions.** If the reason a lint is safe to suppress isn't totally obvious from the surrounding code, leave a brief comment above the attribute explaining why (for instance, noting that a specific cast is mathematically provably in range).
|
||||||
|
- **Never suppress `correctness`-tier lints.** These indicate real defects. Fix the code instead.
|
||||||
|
|
||||||
|
## Documentation style
|
||||||
|
|
||||||
|
- **Objective Tone:** All comments (both doc comments `///` and inline `//`) must be written in a formal, objective, and neutral tone.
|
||||||
|
- **No Personal Pronouns:** Avoid first-person ("we", "our", "us") and second-person ("you", "your") pronouns.
|
||||||
|
- **Voice:** Try to use the passive voice or neutral descriptive language. Instead of "We initialize the buffer," try "The buffer is initialized." Instead of "Your vertex shader needs this," write "The vertex shader requires this."
|
||||||
|
- **Focus:** Describe the code's behavior, the system's state, or technical invariants.
|
||||||
|
- **Struct Documentation:** Every single field in a public or internal struct needs a doc comment (`///`) explaining what it's for and any invariants it holds.
|
||||||
|
- **Function documentation sections:** Function doc comments should follow the standard sections from the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/documentation.html). They should appear in this fixed order after the summary and description: `# Errors`, then `# Panics`, then `# Safety`. These sections apply to **all** functions, whether public or private. Clippy only enforces public ones, but we expect the same standard on private helpers by hand.
|
||||||
|
- **`# Errors`** is mandatory on every function returning a `Result`. It needs to state the exact conditions under which each error variant is returned. `fn main` is exempt from this.
|
||||||
|
- **`# Panics`** is mandatory on any function that can panic. This includes `expect`, `unwrap`, `panic!`, `assert!`, array indexing, or arithmetic that can trip. State the condition that triggers the panic.
|
||||||
|
- **`# Safety`** is mandatory on every `unsafe fn`, stating the exact invariants the caller must uphold.
|
||||||
|
- Test functions (`#[test]`, and helpers inside `#[cfg(test)]`) are fully exempt from all three sections since they aren't part of the documented surface.
|
||||||
|
- We use `missing_errors_doc`, `missing_panics_doc`, and `missing_safety_doc` as warnings in our workspace lint set, so missing a section on a public item will fail CI.
|
||||||
|
- **Stability:** Treat the documentation as a technical specification for the engine.
|
||||||
|
- **Line breaks:** Do not insert line returns inside a comment unless it's genuinely necessary. If a comment fits on a single line, leave it on a single line. Don't artificially wrap text at 80 characters just for aesthetics. Only break across lines when the comment is actually long (like multi-sentence prose or enumerated invariants) or when a hard break carries meaning (like separating an intro line from a bulleted list).
|
||||||
|
|
||||||
|
## Target platforms
|
||||||
|
|
||||||
|
**Linux and Windows only.** We do not support macOS, mobile, consoles, or web/WASM.
|
||||||
|
|
||||||
|
- Both platforms feature native Vulkan support via vendor ICDs (NVIDIA, AMD, Intel). There is no translation layer like MoltenVK, meaning we can adopt modern Vulkan extensions freely without checking a portability matrix.
|
||||||
|
- **File paths:** Always use `std::path::Path` or `PathBuf` along with the `directories` (or `dirs`) crate for looking up user data. Never hard-code paths like `/home/...` or `~`. Linux properly follows XDG standards (`$XDG_DATA_HOME`, etc.), while Windows correctly uses `%APPDATA%`.
|
||||||
|
- **Line endings:** The repository is strictly LF-only. Make sure to set `core.autocrlf = false` and rely on our `.gitattributes` setting `* text eol=lf` to keep diffs completely clean across both operating systems.
|
||||||
|
- **Filename casing:** Never create two files that differ only in casing. Linux is case-sensitive and Windows isn't, so mismatches create incredibly confusing "works on my machine" bugs.
|
||||||
|
|
||||||
|
## Determinism stance
|
||||||
|
|
||||||
|
- **Worldgen is seed-deterministic.** Given the exact same seed, worldgen must produce bit-for-bit the same world on any platform, at any time. This strongly constrains our worldgen code: you must use a fixed RNG algorithm like `wyrand` or `xoshiro`. **Never** use `rand::thread_rng()` or anything seeded directly from the OS. Do not depend on `HashMap` iteration order, as Rust's default hasher is randomized. Use `BTreeMap`, `IndexMap`, or explicitly sort your data when iteration order feeds into RNG draws or content placement. For more detail, check [ADR-0003](docs/adr/0003-seed-deterministic-worldgen.md).
|
||||||
|
- **Simulation is server-authoritative.** The server runs the absolute truth. Clients send their inputs and receive state snapshots back, predicting locally for responsiveness and reconciling whenever they disagree with the server. Combat, physics, mob AI, and item drops are computed exactly once, on the server.
|
||||||
|
- **Full simulation determinism (lockstep, rollback, replay-from-inputs) is a non-goal.** Because of this, floats, hash-map iteration, and platform-specific math are all totally fair game *outside of worldgen*. We don't want to pay the massive performance cost of cross-platform float reproducibility for a feature we aren't even building. See [ADR-0004](docs/adr/0004-server-authoritative-simulation.md).
|
||||||
|
|
||||||
|
## Content IDs & namespacing
|
||||||
|
|
||||||
|
All registered content (like blocks, items, recipes, biomes, and entities) is identified using a **namespaced string** in the exact format `"namespace:id"`. The full rationale for this is in [ADR-0005](docs/adr/0005-namespaced-content-ids.md).
|
||||||
|
|
||||||
|
- **Engine's reserved namespace:** `core:`. All first-party content registered directly by the base game uses this namespace (e.g. `"core:stone"`, `"core:iron_sword"`). Mods pick their own short namespace (e.g. `"mymod:weird_dirt"`).
|
||||||
|
- **Strict form required.** A bare ID with no `:` is considered an **error at registration and parse time**. It will not be silently coerced to `core:`. This same rule applies absolutely everywhere: engine scripts, data packs, Lua mods, recipe references, and save files. There are no exceptions. The symmetry is entirely the point.
|
||||||
|
- **Charset:** The namespace and id must each match `[a-z0-9_-]+`, separated by exactly one `:`. Stick to lowercase ASCII only. No uppercase letters, no Unicode, no spaces, no dots, and no slashes. This keeps IDs easy to grep, completely filesystem-safe, and unambiguous in logs and save data.
|
||||||
|
- **Runtime representation:** We intern each ID string into a small integer handle (like `BlockId(u32)`) when it gets registered. Hot paths should always compare handles, never strings. We keep the original string around purely for display, saving and loading, and the Lua API surface.
|
||||||
|
|
||||||
|
> *Project name note:* The project is named **Synvael** ("Catalyst" was our old working codename). The engine namespace is deliberately kept as `core:` rather than the project name, ensuring it stays stable even if branding changes.
|
||||||
|
|
||||||
|
## Coordinate system & units
|
||||||
|
|
||||||
|
- **Up axis:** **+Y**.
|
||||||
|
- **Handedness:** **right-handed** (this is the default math convention where +X is right, +Y is up, and +Z points toward the viewer or out of the screen).
|
||||||
|
- **World unit:** **1 unit = 1 block.** Blocks are exactly 0.5 meters in physical scale, but inside the engine, everything is counted in *blocks*, not meters. A player is therefore exactly 3 units tall and 2 units wide in world coordinates.
|
||||||
|
|
||||||
|
We've collected implementation gotchas that pop up because neighboring tools use different conventions (like Vulkan clip space, Blender import, or glTF) in [`docs/rendering.md`](docs/rendering.md). Note that these are not convention changes for the engine, just mismatches that we handle in one agreed-upon place.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Rust](https://www.rust-lang.org/tools/install) (stable toolchain, edition 2024)
|
||||||
|
- [Git LFS](https://git-lfs.com/) (binary assets are tracked via LFS)
|
||||||
|
- A Vulkan-capable GPU with up-to-date drivers (Linux or Windows)
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Cryoforge-Nexus/Synvael.git
|
||||||
|
cd Synvael
|
||||||
|
git lfs pull
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p client # windowed client
|
||||||
|
cargo run -p server # dedicated server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test # all tests
|
||||||
|
cargo test -p shared # tests for a single crate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
The CI pipeline enforces strict linting. Run these locally before pushing your code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
selene .
|
||||||
|
stylua .
|
||||||
|
```
|
||||||
|
|
||||||
|
Lua linting requires [Selene](https://kampfkarren.github.io/selene/) and [StyLua](https://github.com/JohnnyMorganz/StyLua). You can install them by running `cargo install selene` and `cargo install stylua`, or by using the pre-built binaries from their GitHub release pages.
|
||||||
|
|
@ -1,55 +1,57 @@
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
Technical, implementation-facing documentation for the Synvael engine. This directory is the **code-side** counterpart to the game-design specification; it describes how subsystems are built, not what the game should feel like.
|
This is the technical, implementation-facing documentation for Synvael. You can think of this directory as the **code-side** counterpart to the game-design specification. It describes exactly how subsystems are built under the hood, not what the game should feel like to play.
|
||||||
|
|
||||||
## How documentation is layered
|
## How documentation is layered
|
||||||
|
|
||||||
Documentation lives at three altitudes. Each layer answers a different question, and content is kept at the layer it belongs to so that no single file accretes everything.
|
Our documentation lives at three different altitudes. Each layer answers a distinct type of question, and we keep content strictly at the layer it belongs to. This ensures no single file turns into an unreadable monolith.
|
||||||
|
|
||||||
| Layer | Location | Answers | Churn |
|
| Layer | Location | Answers | Churn |
|
||||||
|-------|----------|---------|-------|
|
|-------|----------|---------|-------|
|
||||||
| **Project conventions** | [`AGENTS.md`](../AGENTS.md) | "What rules apply no matter which feature I touch?" | Slow, a finite set of cross-cutting invariants |
|
| **Project conventions** | [`DEVELOPMENT.md`](../DEVELOPMENT.md) | "What rules apply no matter which feature I touch?" | Slow, a finite set of cross-cutting invariants |
|
||||||
| **Subsystem technical docs** | this `docs/` tree + Rust module docs (`//!`, `///`) | "How does *this* subsystem work?" | Grows with features, distributed across files |
|
| **Subsystem technical docs** | this `docs/` tree + Rust module docs (`//!`, `///`) | "How does *this* subsystem work?" | Grows with features, distributed across files |
|
||||||
| **Architecture decisions** | [`docs/adr/`](adr/) | "*Why* was this chosen over the alternatives?" | Append-only, one file per decision |
|
| **Architecture decisions** | [`docs/adr/`](adr/) | "*Why* was this chosen over the alternatives?" | Append-only, one file per decision |
|
||||||
|
|
||||||
The rule that keeps `AGENTS.md` lean: if a piece of documentation is specific to one subsystem, it does **not** go in `AGENTS.md`. It goes in that subsystem's module docs or a `docs/<subsystem>.md` note, and `AGENTS.md` only links to it. `AGENTS.md` is an index and a rulebook, not a container for feature specs.
|
We use a strict rule to keep `DEVELOPMENT.md` lean: if a piece of documentation is specific to only one subsystem, it does **not** go in `DEVELOPMENT.md`. It belongs in that subsystem's module docs or a `docs/<subsystem>.md` note, and `DEVELOPMENT.md` only links to it. `DEVELOPMENT.md` is an index and a rulebook, not a dumping ground for feature specs.
|
||||||
|
|
||||||
### Where to put a new piece of documentation
|
### Where to put a new piece of documentation
|
||||||
|
|
||||||
- A rule true across the whole project (a convention, an invariant) → `AGENTS.md`.
|
Not every subsystem gets a dedicated document. You should default to using module documentation unless the technical design spans across multiple modules.
|
||||||
- How one subsystem is implemented → prefer Rust **module docs** next to the code (`//!` at the top of the module). They cannot drift far from the code and render with `cargo doc`.
|
|
||||||
- Cross-file technical design too large for a doc comment (e.g. the rendering frame graph, the network protocol, the worldgen pipeline) → a `docs/<subsystem>.md` note.
|
- A rule that is true across the whole project (like a convention or an invariant) goes in `DEVELOPMENT.md`.
|
||||||
- The reasoning behind a specific, hard-to-reverse choice → an **ADR** in [`docs/adr/`](adr/).
|
- How a specific subsystem is implemented belongs in Rust **module docs** right next to the code (`//!` at the top of the module). These docs can't drift far from the code and they render cleanly with `cargo doc`.
|
||||||
|
- Cross-file technical designs that are just too large for a doc comment (like the rendering frame graph, the network protocol, or the worldgen pipeline) get a dedicated `docs/<subsystem>.md` note.
|
||||||
|
- The reasoning behind a specific, hard-to-reverse choice becomes an **ADR** in [`docs/adr/`](adr/).
|
||||||
|
|
||||||
## Relationship to the design specification
|
## Relationship to the design specification
|
||||||
|
|
||||||
The canonical **design** specification (intent, world rules, gameplay-system behaviour, and unresolved questions) is maintained separately and is **not** part of this repository. This `docs/` tree records how the engine *implements* those designs.
|
The canonical **design** specification (which covers intent, world rules, gameplay-system behavior, and unresolved questions) is maintained entirely separately and is **not** part of this repository. This `docs/` tree exists strictly to record how the engine *implements* those designs.
|
||||||
|
|
||||||
Each subsystem note should name the design topic it implements (by title, e.g. "Design source: *Worldgen*"), so the trail from intent to implementation exists without coupling the repository to an external location. When the implementation and the design disagree, surface the disagreement rather than silently resolving it in code.
|
To maintain the link between intent and implementation, each subsystem note should explicitly name the design topic it implements (by title, like "Design source: *Worldgen*"). This lets us trace the path from design to code without tightly coupling the repository to an external location. If the implementation and the design ever diverge, make sure to document that discrepancy clearly in the relevant subsystem note or ADR, rather than just silently resolving it in code.
|
||||||
|
|
||||||
## Index
|
## Index
|
||||||
|
|
||||||
- [`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/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/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/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/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/0006-base-game-on-modding-api.md`](adr/0006-base-game-on-modding-api.md): Base game built directly on the modding API.
|
||||||
- [`adr/0007-declarative-content-via-modding-api.md`](adr/0007-declarative-content-via-modding-api.md): declarative content loads through the modding API.
|
- [`adr/0007-declarative-content-via-modding-api.md`](adr/0007-declarative-content-via-modding-api.md): Declarative content loads through the modding API.
|
||||||
- [`adr/0008-split-coordinate-entity-positions.md`](adr/0008-split-coordinate-entity-positions.md): split-coordinate entity positions.
|
- [`adr/0008-split-coordinate-entity-positions.md`](adr/0008-split-coordinate-entity-positions.md): Split-coordinate entity positions.
|
||||||
- [`adr/0009-baseline-relative-sparse-chunk-persistence.md`](adr/0009-baseline-relative-sparse-chunk-persistence.md): baseline-relative sparse chunk persistence.
|
- [`adr/0009-baseline-relative-sparse-chunk-persistence.md`](adr/0009-baseline-relative-sparse-chunk-persistence.md): Baseline-relative sparse chunk persistence.
|
||||||
- [`adr/0010-net-crate-async-runtime.md`](adr/0010-net-crate-async-runtime.md): dedicated `net` crate with a confined async runtime.
|
- [`adr/0010-net-crate-async-runtime.md`](adr/0010-net-crate-async-runtime.md): Dedicated `net` crate with a confined async runtime.
|
||||||
- [`adr/0011-authority-stream-for-server-pushed-state.md`](adr/0011-authority-stream-for-server-pushed-state.md): a dedicated authority stream for server-pushed state.
|
- [`adr/0011-authority-stream-for-server-pushed-state.md`](adr/0011-authority-stream-for-server-pushed-state.md): A dedicated authority stream for server-pushed state.
|
||||||
- [`adr/template.md`](adr/template.md): template for new decisions.
|
- [`adr/template.md`](adr/template.md): The template for logging new decisions.
|
||||||
|
|
||||||
Subsystem notes:
|
Subsystem notes:
|
||||||
|
|
||||||
- [`packs.md`](packs.md): data packs & resource packs (load order, layout, resolution).
|
- [`packs.md`](packs.md): Data packs and resource packs (load order, layout, resolution).
|
||||||
- [`rendering.md`](rendering.md): rendering & coordinate gotchas (Vulkan clip space, Blender/glTF import).
|
- [`rendering.md`](rendering.md): Rendering and coordinate gotchas (Vulkan clip space, Blender/glTF import).
|
||||||
- [`chunk_streaming.md`](chunk_streaming.md): chunk streaming and async worker pipeline.
|
- [`chunk_streaming.md`](chunk_streaming.md): Chunk streaming and the async worker pipeline.
|
||||||
- [`meshing.md`](meshing.md): greedy meshing, the mesh worker pool, and frustum culling.
|
- [`meshing.md`](meshing.md): Greedy meshing, the mesh worker pool, and frustum culling.
|
||||||
- [`diagnostics.md`](diagnostics.md): runtime statistics collection and the debug panel.
|
- [`diagnostics.md`](diagnostics.md): Runtime statistics collection and the debug panel.
|
||||||
- [`save_format.md`](save_format.md): chunk persistence, region-file layout, save actor, and load pipeline.
|
- [`save_format.md`](save_format.md): Chunk persistence, region-file layout, save actor, and load pipeline.
|
||||||
|
|
||||||
Further subsystem notes are added here as systems are implemented and locked.
|
We will add further subsystem notes here as those systems are implemented and locked down.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue