Codegen
How TOML manifests become Rust spec code through manifest-schema and codegen-cli, and the build-time override versus run-time data-accessor loop that ties generation to ResolvedGameData
A spec, say Arcane Mage, is defined in two places, and the split is the whole point. The numbers (a cooldown, a coefficient, an aura duration) live in the game data and reach the engine through ResolvedGameData. The behaviour is hand-written in a small TOML manifest: which spells exist, which auras they apply, which talents matter. Code generation stitches the two together: it reads the manifest and emits Rust that, at build time, pulls the omitted numbers out of ResolvedGameData. A manifest is therefore almost all structure and almost no numbers; anything it leaves out is resolved from data.
This figure expands the Engine box of the system-context diagram in Architecture, showing the build-time half of that picture: manifests in, generated Rust out.
The schema: manifest-schema
manifest-schema is the canonical, serde-only definition of what a manifest may contain. It is shared by both the generator and forge's audit tooling, so there is exactly one description of the format. A per-spec manifest deserializes into Manifest; items deserialize into ItemsManifest. Every manifest carries a schema_version, checked against a CURRENT_SCHEMA_VERSION constant so a format change fails loudly rather than generating wrong code.
The per-spec Manifest mirrors a spec's combat definition:
Each section deserializes into its own struct. The recurring theme is that almost every field on a spell or aura is Option, and an absent value means "resolve from game data."
A SpellDef carries id and then a long list of optional overrides:
Cooldown, cost, gain, damage, cast time, GCD, charges, the applied aura, AoE and channel sub-sections, cooldown-reduction rules, and a hook for custom behaviour are all here, and all optional. In a real manifest most of them are absent: the Arcane Mage manifest declares ten spells and fourteen auras where almost every spell gives only its id and lets the generator pull cooldown, cost, cast time, and damage from data. The manifest says what the spec does; the data says by how much.
The generator: codegen-cli
The codegen binary is invoked as cargo codegen and writes into crates/engine-content/src/generated/. Its driver, generate_all, reads every *.toml in the manifests directory, treats items.toml specially, parses each spec file into a Manifest, version-checks it, and calls generate_spec_file. It then emits the items, the barrel mod.rs files, and finally writes every (filename, source) pair to disk. A --check mode regenerates in memory and diffs against the committed files, so CI can prove the generated code is in sync with the manifests without a writable tree.
generate_spec_file emits, in order: the id constants (the SPELL/AURA/TALENT modules), the local-index constants, one aura_* and one spell_* builder function per declaration, the build_combat_system function, the handler factory, and the define_spec_descriptor! invocation. The descriptor registers the spec at link time through the inventory crate, which is how find_descriptor later finds a spec by id without any central registry to maintain.
The build-time override versus run-time accessor loop
The connective idea is worth stating precisely, because it is the reason the two halves stay consistent. When a manifest field is present, the generator emits a literal. When it is absent, the generator emits a call into ResolvedGameData instead. The generated Arcane Mage code, for instance, fills a spell's cooldown with data.cooldown_s(...) and its damage by passing data.damage_def(...) into the builder's s.damage_auto(...), both reading the very same ResolvedGameData that the resolution pass built. cooldown_s and damage_def are ResolvedGameData accessors; damage_auto is the CombatSystemBuilder method that consumes the resolved DamageDef. Each accessor returns an Option, and the generated code converts a None into BuilderError::MissingSpellData, so a spell whose data never resolved fails the build of the combat system rather than silently simulating zeros.
That closes the loop with Data Resolution. At build-combat-system time, the generated builder functions read coefficients, costs, cooldowns, and durations from ResolvedGameData; at resolution time, resolve_game_data populated that value by introspecting the spec the generated code defined. The manifest is the override layer on top, and the game data is the default underneath. Add a number to the manifest and it wins; omit it and the data fills in. And because both halves agree on the spell ids (the generated id constants are exactly the ids the resolver fetched), the override and the default always line up. This is the boundary between the data layer and the engine: from here on, every page assumes the engine is holding a fully resolved ResolvedGameData and a registered spec, and asks what it does with them.
Étapes suivantes
