This section describes the actor's bones, including the hierarchy and pose position.
| Offset | Type | Description |
|---|---|---|
| 0x0 | Magic (0x007B7960 for Sluggers) | |
| 0x4 | uint16 | Actor ID |
| 0x6 | uint16 | Bone count |
| 0x8 | uint32 | First word of an embedded tree structure (unknown purpose) |
| 0xc | uint32 | Root node pointer (relative to header). This is the second word of the tree structure and is used to traverse the bone hierarchy. |
| 0x10 | uint32 | GEO name pointer (relative to header). This is the name of the geometry vertex file the bones are for, <stem>.gpl (or .tpl). Not always NUL-terminated: in 34 vanilla models (10 distinct names, all exactly 12 or 16 characters long) one leftover byte follows the extension before the zero padding, e.g. nokonoko.gpl + B0, L_teresa.gpl + 80, and mii_male.gpl / L_mii_female.gpl + 70 (p, which read as .gplp). 123 other 12/16-character names are terminated normally (survey of 1_Input/dt_na.dat, 2026-10-04). The exporter cuts names after the extension (binfmt.clean_geo_name) for folder names and the .sluggie GeoName; the bytes in the block are never rewritten. |
| 0x14 | uint16 | Skin file id |
| 0x16 | byte[2] | Padding |
| 0x18 | uint32 | User data size |
| 0x1c | uint32 | User data pointer (relative to header). Points at a chain of descriptors — see User data below. |
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Orientation control pointer (relative to header). This is an SRT that holds the bone's pose position, can be null. A null pointer means an identity local transform. It is mostly found on the extra root bones, but non-root bones have it too (chunk 18 bone 2, chunk 95 bone 1). |
| 0x4 | uint32 | Previous sibling bone pointer (relative to header) |
| 0x8 | uint32 | Next sibling bone pointer (relative to header) |
| 0xc | uint32 | Parent bone pointer (relative to header) |
| 0x10 | uint32 | First child bone pointer (relative to header) |
| 0x14 | uint16 | GEO file ID: the index of the GPL submesh this bone draws rigidly. FFFF means the bone owns no mesh (it may still drive skinning of submesh 0 through SKN). See GEO ID ownership rules. |
| 0x16 | uint16 | Bone ID |
| 0x18 | bool | Is this bone positioned relative to its parent? (Usually true) |
| 0x19 | uint8 | Something about draw priority, usually 0 |
| 0x1a | uint16 | Flags. Bit 0x8000 gives the bone two extra matrices multiplied into its draw matrix (set on 15 vanilla bones, never in a stadium); otherwise 0. See How the game builds and draws the bones. |
Surveyed over all 203 models with an ACT section (2026-09-18). The layout is fully deterministic and has no slack:
0x00 header (0x20 bytes)
0x20 bone table: 0x1C x boneCount, in bone id order
0x20 + 0x1C*N SRT array: 0x34 each, one per bone with a non-null SRT, in bone order
geo name, user data, padding (see Section tail)
0..N-1 in table order, with no gaps, aliasing or reordering.0xA14; chunk 95: both 0x5D0). The stride is a uniform 0x34, and no two bones share an SRT. So adding a bone shifts every byte after 0x20 + 0x1C*N, and the section has to be rebuilt, not patched.Surveyed across 721 exported models and confirmed in Dolphin (2026-09-16):
CompCount 3) submesh is named by at least one bone's GEO ID, and its vertices are stored in that bone's local space. Skinned (CompCount 6) submeshes are never named by a GEO ID.Invalid read from 0x00000008, PC = 0x804FF694). A new submesh and its owner's GEO ID must always be written together.SubmeshIndex, unchanged.FFFF into the old owner's) makes the mesh keep its local offset, now relative to the new bone: Mario's cap moved this way sat at the new bone's offset and followed it: from bone 54 to finger bone 49 through hammerspace, and to bone 7 through the in-place patcher (Dolphin, 2026-10-07). To keep the mesh where it was in the rest pose instead, its positions and normals must be re-baked into the new bone's local space (p_new = B_new-1 · B_old · p with the bind matrices); the re-baked cap stayed on the head in the rest pose and followed the hand when animated (Dolphin, same day). Restoring the two GEO IDs restores the original placement.sta03 bone 0, sta03b bone 21). Free vanilla roots are few: one in Yoshi Park day (sta03 bone 0), 19 in its night model, two in Daisy Cruiser day (sta08 bones 6 and 7, after every mesh in draw order), none in any Mario Stadium model (sta01/b/c: every bone owns a mesh). Dolphin results:
builtin:rigid_spec_v1 list with Spec, s16 positions, white RGBA4444 vertex colours and normals, as a cube or a flat plane (2026-10-01).sta03 submesh 5 (the field markings) moved from bone 6 onto a new root bone 7 with bone 6's SRT drew unchanged, and raised with that bone it floated above the field, so it is drawn from the new bone (2026-10-18). A custom cube on a new root bone draws solid in Yoshi Park day and Daisy Cruiser day, from the Blender add-on workflow too (2026-10-18).The bone table is reached through the tree: the tree header sits at header+0x08 and its root pointer at header+0x0C. Each node address is the bone's start, relative to the ACT header. BlockValidator walks it to check the rules above.
From main.dol (US; function names from the external tool's symbol list, 2026-10-14). Stadiums are built exactly like characters: nothing outside the model block lists their bones. No DOL table (stadium archive, object lists, options, time of day) names bones, and the GPL mesh names do not occur in the DOL.
fn_804EFD24). fn_804EF3FC relocates the ACT pointers and overwrites header +0x08 with 4 (the prev/next/parent/child links start at bone +4), so the vanilla 0xC or stale-looking value there does not matter. It makes boneCount (header +6) bone objects of 0x190 bytes and fills them by walking the tree from the root pointer through next and child links (fn_804F08D8), so a new root linked at the end of the root chain is found like any other.fn_804ED670). It is set on 15 vanilla bones in the whole dt_na.dat, never in a stadium; new bones write 0.0..boneCount-1 whose GEO ID is not 0xFFFF, construction pushes a 6-byte draw entry {u16 geo, u16 bone, u8 visible = 1, u8} (vector at actor +0x7C). The render function fn_804F43A8 draws the entries in that order, so a mesh on a new bone always draws last. No game code writes the visible byte or the display object's flag after construction.fn_804F2BB8): each local matrix from its SRT, times the parent's; roots use the actor matrix. A mesh is drawn with its bone's world matrix and skipped only when its visible byte or display-object flag is 0, or when it has culling bounds and fails the frustum test (see GPL user data).Each player character has a high-poly model and a low-poly L_* model, stored as file indices n and n + 1 of the same chunk. Their ACT geo names match once the L_ prefix and the extension are removed; names are compared after cleaning (see the GEO name pointer above; the Mii pairs read as .gpl/.gplp without it). All 66 vanilla pairs have the same bone count, and 56 have identical parent, track and mirror tables.
The game poses the low-poly model with the high-poly model's bones. Confirmed in Dolphin with Mario (2026-09-27), after appending a bone 91 to one or both models:
L_mario with a mesh on its new bone 91 crashed on model load while HP Mario had 91 bones (0-90). The same mesh on existing bone 49 loaded, even with the unused bone 91 still present. Adding bone 91 to HP Mario as well made the mesh on bone 91 load. An extra bone that owns no mesh is harmless.L_mario's bone 91 still parented to the head, L_mario's mesh followed the arm.L_mario had only 91 bones worked (Dolphin, 2026-09-18).The hammerspace patcher checks the pair against the partner's currently patched state. It refuses a patch or unpatch that would leave the low-poly model drawing on a bone the high-poly model lacks. It warns when an appended bone's parent or SRT differs between the two models.
Found while investigating (2026-09-18) what silently counts bones outside the header's own boneCount. The header's user data pointer (0x1c) is null when userDataSize (0x18) is 0 — dozens of models ship no user data at all with a perfectly working bone tree. When present, it points at a chain of descriptors, each read until the header's total userDataSize is consumed:
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint32 | Descriptor size in bytes, including this 0x0C-byte header |
| 0x4 | uint16 | Kind (see below) |
| 0x6 | uint16 | Count (meaning depends on kind) |
| 0x8 | uint32 | Data pointer (relative to header) |
Three kinds are seen across the corpus:
uint16[boneCount], indexed by bone id, giving the ANM track id that bone's animation reads from. 0xFFFF means "no track" — a normal, shipped state (every player model has some: 13-14 trackless bones on Mario). This is the one kind act_section.html previously alluded to as "linking information between bones and animation tracks."
(uint8 mirror_bone_id, uint8 role)[boneCount], indexed by bone id. Almost certainly the left/right mirroring used for lefty/righty batting stances. On every player model checked, this table is a perfect involution: table[table[i]] == i for every bone. role values seen: 1 = the main root, 2 = one of five extra root bones, 3 = an ordinary bone (a bone that mirrors itself, i.e. a centreline bone, is simply (own_id, 3) — 23-38 of these per model), 0/4 = rare one-offs of unknown meaning (chunk 18 bone 54 is role 0; chunk 95 bone 17 is role 4). Two donors (chunk 37/1 and 51/1, plus three Magikoopa recolor siblings 52/1, 53/1, 54/1) have oversized, non-involution mirror tables with (0,0) holes and duplicate bone ids — these do not fit this model and are treated as out of scope by tooling that rebuilds this section. Because mirror ids are stored as uint8, bone ids in a rebuilt skeleton must stay <= 255; a corrupt mirror table was confirmed in Dolphin to fail silently (no crash, no visible glitch), so there is no in-game signal to catch a bad table after the fact.
(uint16 track_id, uint16 = 2), with the descriptor's own count field matching (size - 0x0C) / 4. It references animation tracks, not bones, so it is unaffected by the bone count and can be treated as an opaque blob to copy through verbatim.
The kinds are independent: some models carry only a kind-3 table and no mirror table (chunks 130/18, 137/74, 137/139, 142/129). Models without any user data include chunks 5, 7, 9-12, 15, 16, 126-129, 137, 142 and 159.
Every player model's bone table is exactly boneCount entries, ids 0..boneCount-1 in table order with no gaps — the mirror and track tables are the only place a bone count appears outside the header, and this is why extending the bone table (e.g. appending new bones) requires rebuilding kinds 2 and 3 alongside it, while any kind-4 entry can simply be carried through unchanged.
SRT stands for scale, rotation, transform.
| Offset | Type | Description |
|---|---|---|
| 0x0 | uint8 | Type: a component-presence mask, see SRT type byte |
| 0x1 | byte[3] | Padding |
| 0x4 | float[3] | Scale (x, y, z) |
| 0x10 | float[4] | Quaternion stored as (x, y, z, w). Note: when applying this quaternion, negate w and reorder to (-w, x, y, z) to match the game's convention. |
| 0x20 | float[3] | Translation (x, y, z) |
| 0x2c | byte[8] | Padding? Don't know why it's here. |
The type byte says which components the game reads. Bit 0x4 means "rotation is live", bit 0x8 means "translation is live". The game ignores any component whose bit is not set. Surveyed over the player corpus (about 4,000 bones with an SRT, 2026-09-19):
| Type | Bones | Non-identity rotation | Non-zero translation |
|---|---|---|---|
| 0 | 488 | 0% | 0% |
| 4 | 81 | 100% | 0% |
| 8 | 1469 | 0% | 62% |
| 12 | 1903 | 100% | 94% |
There are no exceptions in either direction. Types with bit 0x1 set (1, 5, 9, 13) have identity scale, rotation and translation everywhere and mean something else. No vanilla bone has a non-unit scale, so no scale bit is known.
So a new bone's type byte must be derived from the values written, never copied from its parent. Found in game: a new bone that copied its translation-only parent's 8 lost its own rotation, and the mesh on it rendered at Mario's chest instead of above his cap, rotated 180 degrees about the bone. The Blender exporter now computes the mask from the bone's rotation and translation, and warns about a non-unit scale.
New bones can be appended to a skeleton as long as nothing about the existing bones changes. Confirmed in Dolphin (2026-09-18 to 2026-09-20):
0xC0, an id no stock bank uses) and a bank-7 sequence keyed for it in Blender, with Mario's belly weighted to it, bent the belly as keyed (Dolphin, 2026-10-13). The order matters for the tools: the model carries the track id, so the bank is checked against the patched model and must be written after it.sta03_yoshi_train02, 3 bones) all worked with a submesh on a new bone.Why appending is safe: nothing outside the ACT counts bones. Player models have ptr6 = ptr8 = 0, and ptr7 (facial poses) indexes submeshes, not bones. GPL, TEX and SKN have no bone counter. SKN refers to bones by id (and a new bone may be skinned to, 2026-10-12), ANM is looked up by track id (a new bone has track 0xFFFF and is skipped unless it is given one), and the bat/glove attach points in main.dol use bone ids too. Keeping every existing id keeps all of these valid. The one exception is the high-/low-poly pair, see above.
Changing existing topology is not safe, and the result is silently wrong rather than a crash:
Appending a new bone at the end of its parent's child chain adds a link without disturbing any existing one, so it is the only topology change allowed.
New root bones, for stadiums. A new root bone is written with a null parent pointer and linked after the last root in the root sibling chain, which starts at the header's root pointer (+0x0C). That adds a link at the end of an existing chain, like a leaf append. The header word at +0x08 is left alone: across all exported models it is 0xC or a stale-looking address, and the game overwrites it with 4 at load (see How the game builds and draws the bones). act_rebuild.append_leaf_bone(parent_id=None) builds this layout. Meshes on new root bones draw correctly in Yoshi Park and Daisy Cruiser (Dolphin, 2026-10-18; see GEO ID ownership rules). The patcher takes new root bones only in stadium models (dirs 7-16), whose bones are all roots, and refuses them elsewhere.
The hammerspace patcher enforces this before writing:
uint8 ids), and every new bone has a parent. In stadium models (dirs 7-16) it is the other way round: a new bone must be a root, and new child bones are refused (untested there).0xFFFF, or a track id of its own from 0xC0-0xFF (no stock skeleton or bank uses these: ids in use are 0x00-0x8C, 0x100-0x127, 0x180-0x199 and flag-style 0x4xxx/0x8xxx/0xCxxx; survey 2026-10-12), and mirrors itself with role 3.boneCount-long involution.The ACT section starts on a 32-byte boundary, like every section (see overview). Its inside has no single alignment formula. The geo-name strings follow the SRT blob directly, with no reserved null-terminator byte in the layout. The padding after the names and user data, up to the section end, varies per donor. It depends on the section that follows ACT in the model block, not on anything inside ACT (185 player models rebuilt byte-identical this way).
act_rebuild therefore recomputes the bone table and SRT blob, which shift when the bone count changes. It carries the name/tail region through verbatim and leaves section alignment to BuildHEADERModelBlock.