BPAT is the baked runtime animation format used for skinned creatures in Standard of Iron. A BPAT file packages the data the runtime needs to animate a creature without rebuilding authored skeletal animation from source definitions every frame.
The format stores pre-baked bone palettes for named animation clips together with clip timing, animation markers, variant metadata, attachment sockets, ground-contact data, bind-pose data, and the skeleton parent table.
At runtime, the game chooses a clip and phase, reads/interpolates the baked pose data, and uses the same asset for rendering, attachment placement, grounding, combat timing, and layered animation where those systems require it.
The binary contract is defined by:
animation/bpat/bpat_format.h β structures, constants, IDs, sizes, version;animation/bpat/bpat_reader.cpp β validation and decoded runtime view; andtools/bpat_baker/ β production writer/bake path.Generated files are outputs of that contract. When the code and an older .bpat disagree, the current format/reader code is authoritative.
animation/bpat/bpat_format.h defines:
k_magic = {'B', 'P', 'A', 'T'}
k_version = 3
The current reader accepts exactly k_version.
A file with a different version is rejected with an unsupported-version error rather than interpreted through a best-effort compatibility path.
That strictness keeps the binary layout deterministic: if the structure changes, the version must change with it.
The current species/profile IDs are:
| ID | Species/profile |
| 0 | humanoid |
| 1 | horse |
| 2 | elephant |
| 3 | humanoid sword-ready |
| 4 | humanoid spear-ready |
| 5 | humanoid skeleton |
| 6 | humanoid caster |
| 7 | humanoid stave-caster |
| 8 | sheep |
| 9 | wolf |
k_species_count is 10 and k_max_species_id is the wolf ID.
The profile IDs allow several humanoid bake variants to coexist while still using the same BPAT runtime format.
A BPAT v3 file is laid out as a header plus referenced sections:
BpatHeader (64 bytes)
BpatHeaderExtV3 (32 bytes)
β
ββ clip table
ββ socket table
ββ string table
ββ frame bone palettes
ββ optional/permitted per-frame socket transforms
ββ frame contact table
ββ bind palette
ββ bone-parent table
The writer aligns sections using k_section_alignment, currently 16 bytes.
Offsets in the header are what define the binary section locations. Consumers should not infer section placement from the size of one generated sample file.
The 64-byte BpatHeader identifies core file facts, including:
These fields are sufficient to locate the original BPAT sections and validate the broad dimensions of the asset.
BpatHeaderExtV3 is 32 bytes and extends v3 with offsets/counts for:
These additions let the runtime recover more than GPU skinning matrices. It can derive local poses, bone-global transforms, hierarchy-aware interpolation, and grounding/contact information from the same baked asset.
Each BpatClipEntry is 48 bytes.
A clip entry stores:
All clips share one global frame stream. The clip entry identifies the contiguous frame range belonging to that action.
The five normalized markers are:
| Marker | Meaning |
anticipation_start |
wind-up begins |
weapon_release |
weapon begins travelling toward the target |
contact |
impact/contact point used by melee timing |
recover_unlocked |
recovery/chaining may begin |
exit_safe |
clip can be interrupted/blended out safely |
These markers make action timing authored data rather than a runtime guess based on clip names or fixed frame numbers.
A melee system, for example, can use the baked contact/recovery semantics even if two actions have different frame counts or FPS.
Markers are stored as normalized clip phases rather than absolute wall-clock times.
This lets runtime code map them consistently onto the current clip duration/playback rate.
An unset marker uses the value produced by the marker source/bake path. Consumers should read marker state from the baked entry instead of assuming every action supplies every timing landmark.
Bit 0 of the flags byte is:
k_clip_flag_supplies_ground_contact
This marks clips whose baked contact data supplies the relevant grounding/contact information for the runtime path.
Flags belong to the binary clip contract; they should be expanded through the format definition rather than through undocumented magic values in a consumer.
Variant metadata records:
BpatBlob::clip_is_variant_of() validates that a requested clip really belongs to the expected family before runtime code treats base + ordinal as a valid variant.
This avoids relying on accidental clip-table adjacency as the only proof that several actions are interchangeable variants.
For every stored global frame, BPAT contains one skinning matrix per bone.
The layout is conceptually:
frame 0: bone 0 ... bone N-1
frame 1: bone 0 ... bone N-1
...
Clips select slices of that global frame stream using frame_offset and frame_count.
BpatBlob::palette_matrices() exposes the decoded palette block.
A skinning matrix is not automatically the same thing as the bone's global pose matrix.
bone_global_matrix(frame, bone) combines the stored skinning transform with the baked bind-pose information when a consumer needs the actual global bone pose.
This is important for systems such as attachment sockets or analysis that need the transformed bone rather than the GPU-ready skinning matrix alone.
Version 3 can store one bind-pose matrix per bone.
The bind palette provides the reference needed to recover posed/global/local information from the baked skinning palette.
The reader validates that the bind-palette section fits the file and matches the bone dimensions expected by the header.
Version 3 also stores one parent byte per bone.
The special value:
0xFF
identifies a root bone.
The reader rejects a parent index that does not precede its child in the baked ordering.
This ordering constraint lets the runtime reconstruct hierarchy-dependent pose data in a deterministic forward pass.
Using the palette, bind pose, and hierarchy, the reader can derive local bone transforms.
frame_local_pose_view() exposes decoded local rotations/translations used by interpolation and layered animation code.
This is one reason the v3 asset contains more than the matrix block required for simple GPU skinning: the runtime can work with bone-local pose data without returning to the authoring source.
BpatFrameContact is an 8-byte record with:
sole_y; andfoot_y.sole_y represents the lowest posed sole point relative to the bind-pose reference used by grounding.
foot_y records the lowest foot-bone origin used by the preparation/shadow path.
When a contact table is present, the reader requires exactly one record per global frame.
That one-to-one relationship keeps contact sampling aligned with animation sampling.
Grounding information can be expensive or inconsistent to infer from arbitrary posed geometry at runtime.
Baking it gives the runtime a stable per-frame answer that can be shared by animation preparation and grounding/shadow logic.
The value still belongs to presentation/animation behavior; authoritative unit-root position remains a gameplay/movement fact.
Each BpatSocketEntry is 32 bytes.
A socket stores:
When sockets are present, the file also contains their per-frame transformed data after the palette section according to the current writer layout.
The reader checks that every socket anchor references a valid bone.
Sockets provide stable attachment points for items/effects that need to follow the animated skeleton.
Typical consumers include weapon/equipment placement and other creature attachments.
The socket contract keeps attachment placement tied to the baked skeleton rather than hard-coded world offsets in individual renderers.
Clip and socket names are stored through offsets into the file string table.
The reader verifies that names:
This prevents malformed offsets from turning a corrupt asset into an unbounded string read.
BpatBlob::validate() rejects a blob when the current binary contract is violated.
Current checks include:
k_version;bone_count equal to zero or above 64;frame_total;frame_total;A BPAT file is therefore treated as an untrusted binary blob until those structural checks pass.
Clip frame ranges are required to be contiguous in the global frame stream.
That gives the runtime a simple model:
clip 0 frames
clip 1 frames
clip 2 frames
...
with no holes or overlapping clip ranges.
The sum of all clip frame counts must equal frame_total.
The reader currently accepts at most 64 bones.
That limit is part of the format/runtime contract and should be treated as such when authoring a new creature skeleton.
A source rig with more bones cannot simply be baked and expected to load unless the format/runtime limit is changed coherently.
Runtime systems select clips using the baked manifest/profile and action state.
The BPAT reader supplies:
Higher-level animation code decides which clip to play and how to blend/layer it. The BPAT file supplies the immutable baked data.
The local-pose view enables runtime interpolation and layered animation without rebuilding the authored animation graph.
For example, a defensive upper-body overlay can compose with locomotion when the runtime has the relevant local bone pose data and masks/overlay rules.
The BPAT format itself does not decide gameplay state; it makes the posed animation data available to the animation/rendering systems that interpret current actions.
The normal repository command is:
make bake-bpat
The direct executable is:
./build/bin/bpat_baker [output-directory]
The current CLI accepts one optional output directory.
It does not accept a species selector.
tools/bpat_baker/main.cpp always bakes the complete built-in profile/species set.
The baker iterates six humanoid bake profiles and then bakes horse, elephant, sheep, and wolf.
The resulting BPAT files are:
humanoid.bpat;humanoid_sword.bpat;humanoid_spear.bpat;humanoid_skeleton.bpat;humanoid_caster.bpat;humanoid_stave_caster.bpat;horse.bpat;elephant.bpat;sheep.bpat; andwolf.bpat.These correspond to the current supported species/profile IDs.
The same bake target also produces minimal .bpsm snapshot meshes for:
Current generated names include:
horse_minimal.bpsm;elephant_minimal.bpsm;sheep_minimal.bpsm;wolf_minimal.bpsm.These assets belong to the creature presentation/LOD pipeline rather than the BPAT matrix format itself, but they are generated by the same production bake target.
The bake target also produces full/minimal .bprm rigged bodies for the relevant creature profiles, including humanoid/skeleton-humanoid and the four non-humanoid creatures.
This keeps animation and body outputs synchronized through one asset-generation step.
CMake runs the baker for both:
assets/creatures
and:
build/bin/assets/creatures
so the repository/source asset tree and the staged runtime asset tree receive the generated outputs expected by the build/run workflow.
CreatureBakeRecipeA baked creature profile is driven by a CreatureBakeRecipe and its runtime manifest.
The recipe provides information such as:
The baker then writes the manifest's configured bpat_file_name and associated body/snapshot assets.
Adding a new built-in profile requires more than assigning a new species ID.
The current integration points include:
CreatureBakeRecipe;tools/bpat_baker/main.cpp;tools/bpat_baker/CMakeLists.txt; andBecause the CLI bakes the complete built-in set, the new profile becomes part of the normal bake invocation rather than a separately selected command-line mode.
A binary-format change should be treated as a versioned contract change.
At minimum, review:
BpatHeader / extension structures;k_version;The reader currently accepts only one exact version, so adding an incompatible section/field without a version change is not a safe migration strategy.
A BPAT failure can usually be localized from the reader validation stage.
The file was baked with a format version different from the runtime k_version.
Header offsets/counts do not fit inside the file. Inspect writer layout or a truncated/corrupt asset.
Clip offsets/counts no longer form one contiguous frame stream.
The baked skeleton parent table violates the parent-before-child ordering required by the decoder.
Inspect string table offsets/NUL termination or socket anchor indices.
Regenerating the asset with the current baker is the first useful check when a stale generated file is suspected.
The current BPAT pipeline depends on these invariants:
1..64;frame_total exactly;make bake-bpat.
| Concern | Source |
| Binary structures/version/IDs | animation/bpat/bpat_format.h |
| Reader/validation/decoded views | animation/bpat/bpat_reader.cpp |
| Bake executable | tools/bpat_baker/main.cpp |
| Generated-output list | tools/bpat_baker/CMakeLists.txt |
| Creature manifests/recipes | animation/creature bake sources |
| Runtime creature rendering | renderer/animation creature paths |
The current v3 structures, reader checks, and production baker are the BPAT contract. Older generated-file assumptions or partial output lists should not be carried forward when they disagree with those sources.