Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Contributing

Welcome to the Rakata workspace! This guide outlines how we build, how we test, and the core rules for keeping our code clean, compliant, and maintainable.

License policy

  • License: All workspace crates use GPL-3.0-or-later.
  • Third-Party Components: New dependencies must be compatible (MIT, Apache-2.0, BSD). Add them to THIRD_PARTY_NOTICES.md before merge.

Clean room implementation

To ensure everything we build is 100% our own original work and we aren’t accidentally borrowing from other community tools (if you’re curious about why we’re so strict about this, check out docs/src/legal.md):

  1. Reference Policy: Treat existing tools (like PyKotor) as behavioral references, not copy sources.
  2. No Copy-Paste: Do not copy source code blocks, large comments, or docstrings from third-party sources into Rust files.
  3. Re-Derivation: Derive implementation logic from format documentation, observed behavior (hex dumps), and black-box fixture analysis.
  4. Reverse Engineering:
    • Behavior verification via disassembly tools (e.g., Ghidra) is allowed for interoperability analysis.
    • Do not copy decompiled code into source files.
    • Record findings as paraphrased behavior notes natively within the relevant format specification under docs/src/formats/.

What belongs in the engine audits

The entire Rakata format specifications manual (docs/src/formats/) serves as the engine audit layer between reverse engineering and implementation. All Rust code is written strictly from these engine audits (specifically the Engine Audits & Decompilation sections embedded in each format’s blueprint), not from raw decompilation output.

  • Record: Field names, data types, default values, error conditions, and observable behavioural rules (e.g., “field X is clamped to 0-100”, “list is sorted ascending by field Y”).
  • Do not record: Step-by-step algorithmic sequences, control flow structure, or implementation details that go beyond what is needed for interoperability. The test is: could someone implement correct behavior from this note without it dictating a specific code structure?

Format work versus engine reimplementation

Right now this workspace is only about format parsing, linting and modding tools: reading, writing and validating the game’s data files.

An engine replacement, with gameplay logic, AI and rendering, is a job for another day. That is part of why the format blueprints matter: somebody building an engine later can work from the engine audits rather than digging through decompiled binaries themselves.

Code style and linting

Pre-commit hooks

We use pre-commit to keep the codebase consistently formatted without anyone having to manually police it. After cloning the repository, it’s highly recommended to set up the hooks:

pre-commit install
pre-commit install --hook-type pre-push

This registers two quick automated stages:

  • pre-commit: Formats your code via cargo fmt --all (auto-fixing it for you) and runs cargo clippy across all targets.
  • pre-push: Runs cargo test --workspace --all-features to ensure tests are green before you push.

Try to avoid skipping hooks using --no-verify. If a hook catches something, it’s usually just a helpful clippy suggestion or a quick formatting tweak!

Manual checks

If you prefer running things manually from the workspace root before committing:

cargo fmt --all
cargo clippy --workspace --all-targets --all-features
cargo test --workspace --all-features

Note: Passing --all-features to clippy and test is important so it catches optional code paths like serde and tracing! We just ask that fmt and clippy run cleanly before you open a Pull Request.

Idiomatic Rust

A few principles the codebase leans on:

  • Safe Numeric Casts: To prevent silent truncation bugs, we enforce #![warn(clippy::as_conversions)]. Avoid the raw as keyword; lean on From, TryFrom, or .into(). If an unsafe cast is truly unavoidable (like an f32 down to an i32), use a scoped #[allow(clippy::as_conversions)] and drop an inline comment explaining why it’s safe.
  • No Primitive Obsession: We heavily utilize strongly-typed wrappers (like ResRef) rather than passing raw [u8; 16] or String primitives around.
  • Strict Error Handling: We explicitly forbid .unwrap() and .unwrap_unchecked() in library code. Everything must propagate cleanly via Result using typed error enums (managed via thiserror).
  • Composition over Hierarchy: We prefer flat structs and trait combinators over deep class hierarchies.
  • Honest Projections in Typed Views: Typed views over GFF in rakata-generics (Utc, Uti, Are, Git, Dlg, Ifo, Utd, Ute, Utm, Utp, Uts, Utt, Utw) model only the fields they enumerate. from_gff silently drops unmodelled fields and to_gff writes only the modelled ones. Do not add an extra_fields accumulator on the struct; callers that need byte-exact preservation work with the raw Gff tree directly. See Typed Views and Raw GFF for the rationale.
  • Iterators over Loops: We prefer functional iterator chains (map, filter, fold) over maintaining manual mutable state in for loops.
  • Zero-cost Features: Optional functionality (like serde serialization or tracing telemetry) must introduce no overhead when disabled.
  • Safe by Default: We use #![forbid(unsafe_code)] across all core parser crates to enforce strict memory safety boundaries.

Testing

Tests here are gray box: white-box knowledge of the engine, taken from the audits under formats/, drives strictly-validated black-box cases. See Testing for what to include with a new format, and for the ways a passing test can fail to check anything.

The reserved field rule

Game engines are weird, and sometimes they leave mysterious “padding” or “reserved” sections in their binary formats. Every struct field that corresponds to a reserved region must be:

  • Stored strictly as a named array (e.g., reserved: [u8; N]) in the format struct.
  • Read directly from the source bytes verbatim.
  • Written back verbatim during a roundtrip.

If a writer zeroes out or silently drops a reserved field you parsed, we consider that a “lossless bug”, even where the engine appears not to use those bytes. If you’re constructing a brand new file from scratch, you can safely write zeroes for reserved regions, but the struct must be capable of storing exactly what it read off disk.

Release process

(TODO: We haven’t cut an official production release yet. Right now we are building out the rakata-lint engine rules and expanding format coverage. Once we officially stabilize v0.3.0 to crates.io, we’ll formalize our exact release checklist, dependency license refreshes, and CI pipelines here.)