wh40kdc 0.4.8

Warhammer 40K dataset for the 40kdc-data schema layer: generated types, an embedded dataset behind a linked typed API, plus ListForge + NewRecruit roster importers and exporters.
Documentation

wh40kdc

The Warhammer 40K dataset for the 40kdc-data schema layer — the canonical community schema for 40K game entities. This crate is the Rust counterpart of the @alpaca-software/40kdc-data npm package: it ships the generated entity types, the whole dataset embedded behind a linked, typed API, and a ListForge army-list importer.

Every type is generated from the project's JSON Schemas (JSON Schema draft 2020-12) via typify, so the Rust structs stay in lockstep with the schemas that other 40kdc tools validate against.

Data API (the headline)

With the default features, the entire dataset is embedded at build time behind a linked accessor. Find an entity and follow it to its faction, weapons, abilities, and the phases each ability acts in. Lookup is diacritic- and punctuation-insensitive, so "Kharn" resolves "Khârn the Betrayer":

use wh40kdc::{Dataset, Phase};

let ds = Dataset::embedded();
let kharn = ds.find_unit("Kharn").unwrap();

let shooting: Vec<&str> = ds
    .abilities_of(kharn)
    .into_iter()
    .filter(|a| ds.phases_of(a).contains(&Phase::Shooting))
    .map(|a| a.ability_id.as_str())
    .collect();

assert_eq!(shooting, ["berzerker-frenzy"]);
assert_eq!(ds.faction_of(kharn).unwrap().id.as_str(), "world-eaters");

An ability's phases are not stored on the ability — they come from phase-mappings, joined for you by [Dataset::phases_of]. Name matching is shared with the npm package via the exported [normalize_name] (NFD diacritic strip, casefold, quote/whitespace normalization).

Army-list importer

The import feature turns a ListForge "share" export (URL, base64 segment, or raw JSON) into a resolved 40kdc Roster, keyed on entity ids and validatable against roster.schema.json. It reads only an allowlist of structural fields, so it never carries reproduced rules text into its output.

use wh40kdc::Dataset;
use wh40kdc::import::import_listforge;

let roster = import_listforge(share_url, Dataset::embedded()).unwrap();
println!("{} of {} units resolved",
    roster.diagnostics.resolved_units, roster.units.len());

The importer is multi-format by design: decode and resolve are written once, and each source format (ListForge today; New Recruit, Rosterizer, … next) is a small FormatAdapter that lowers a payload to the shared ParsedRoster.

Types-only usage

To consume just the generated types (no embedded data, no extra dependencies), disable default features:

[dependencies]
wh40kdc = { version = "0.2", default-features = false }
serde_json = "1"
use wh40kdc::{Unit, Weapon};

let units: Vec<Unit> = serde_json::from_str(&units_json)?;
let weapons: Vec<Weapon> = serde_json::from_str(&weapons_json)?;

The bundled schema is available as a string for downstream validation:

let schema: serde_json::Value = serde_json::from_str(wh40kdc::BUNDLED_SCHEMA)?;

Damage projection

The cruncher feature ships the Rust mirror of the npm package's expected-value damage engine. Closed-form math over a flat Buff stack — no sampling, no I/O. The cross-implementation conformance corpus pins both engines to within 5e-4 per stage.

use wh40kdc::{Dataset, Phase};
use wh40kdc::cruncher::{crunch, AttackProfileRef, EngineContext, EngineInput, TargetProfileRef};

let ds = Dataset::embedded();
let weapon = ds.find_weapon("bolt-rifle").unwrap();
let target = ds.find_unit("intercessor-squad").unwrap();

let ctx = EngineContext {
    phase: Phase::Shooting,
    attacker_stationary: Some(false),
    attacker_charged: None,
    within_half_range: Some(false),
    attacker_in_cover: None,
    target_in_cover: None,
    attacker_keywords: None,
    target_keywords: None,
    timing: None,
    attacker_attached: None,
};
let out = crunch(&EngineInput {
    attacker: AttackProfileRef { weapon, profile_index: 0 },
    target: TargetProfileRef { unit: target, profile_index: 0, model_count: None },
    models_firing: 5,
    buffs: Vec::new(),
    context: ctx,
}, None).unwrap();
// out.stages: attacks → hits → wounds → unsaved → damage → after-fnp → models-killed

Cargo features

  • bundled-data (default) — the embedded dataset and the linked data API (Dataset, Collection, normalize_name).
  • import (default) — the army-list importer (implies bundled-data).
  • export (default) — the roster exporter (NewRecruit JSON / wtc-compact / wtc-full / simple / canonical Roster JSON / Rosterizer). Dataset-free.
  • cruncher (default) — the expected-value damage engine (implies bundled-data).

Disable all four with default-features = false for a types-only build.

Regenerating

Two artifacts are checked in. To regenerate after a change:

cd tools && npm run bundle:schemas   # rebuild crates/wh40kdc/schemas/bundled.schema.json
cargo run -p xtask -- codegen        # rewrite src/generated.rs from the schema
cargo run -p xtask -- bundle-data    # rewrite src/data/bundle.generated.json from data/

CI fails if the committed artifacts drift from the schemas or the data.

Licensing

  • The crate code is MIT.
  • The schema content these types describe is CC0 (public domain).
  • The embedded dataset mixes two licenses: the structural core data is CC0, and the community-authored enrichment data (abilities, phase-mappings, and the ability mechanics they encode) is CC BY 4.0. If you redistribute the embedded data (e.g. by depending on this crate with the default bundled-data feature), attribute Alpaca Software and the 40kdc community. The types-only build (default-features = false) carries no data and so no attribution obligation.

This dataset is community-created and mirrors Games Workshop's datasheet structure. Stat lines and points are numerical facts; no rules text or ability prose is reproduced.