Studio header

The root of every Mdl file. studiohdr_t sits at file offset 0 and is always exactly 408 bytes long. Nearly every other structure in the file is reached by following one of its *index fields.

Offsets. Every *index field in the studio formats is a byte offset relative to the base of the structure that contains it, not to the start of the file. The two happen to coincide here only because studiohdr_t lives at offset 0; the distinction becomes real as soon as you follow an index into a nested structure. An index of 0 means null, not offset zero. Counts and indices come in pairs (numbones / boneindex), and a count of 0 leaves its index meaningless.

Runtime fields. Several fields are scratch space that the engine writes into once the file is mapped into memory: virtualModel, animblockModel, pVertexBase and pIndexBase are pointers, and activitylistversion / eventsindexed are caches. They are zero in every shipped file examined so far, and this library writes zeros back. The pointer fields are 4 bytes because the on-disk layout is the 32 bit one; 64 bit builds of the engine relocate them into studiohdr2_t precisely so that the file layout does not change.

Versions. version is a plain integer, not a bitfield. Team Fortress 2 ships versions 44 through 48, and 48 accounts for the overwhelming majority. The layout of studiohdr_t itself does not change across that range – version differences live in the interpretation of other structures, so this header parses identically for all of them.

studiohdr2_t exists because Valve ran out of room in the 408 byte header and did not want to bump the file version, which would have invalidated every shipped model. studiohdr2index points at it, 0 meaning absent, and the engine guards every access on that check. The SDK notes an intention to fold these fields back into studiohdr_t “next time we up the .mdl file format”, which never happened.

struct valvemdl.structs.studiohdr.studiohdr_t
id(bytes[4], constant=b'IDST')

magic number; IDSTUDIOHEADER

version(int)<parsed from long>
checksum(int)<parsed from long>

must match the checksum in the vvd, vtx and phy files for the engine to load them together

name(string)<parsed from a 64 bytes long padded string>

stored inline as a fixed 64 byte buffer, not as an offset; see studiohdr2_t.sznameindex for the overflow path

length(int)<parsed from long>

total size of the file in bytes

eyeposition(Vector)

ideal eye position

illumposition(Vector)

illumination center

hull_min(Vector)

ideal movement hull size

hull_max(Vector)
view_bbmin(Vector)

clipping bounding box

view_bbmax(Vector)
flags(int)<parsed from unsigned long>
Represents a flags Enum
0x1STUDIOHDR_FLAGS_AUTOGENERATED_HITBOX
0x2STUDIOHDR_FLAGS_USES_ENV_CUBEMAP
0x3STUDIOHDR_FLAGS_FORCE_OPAQUE
0x4STUDIOHDR_FLAGS_TRANSLUCENT_TWOPASS
0x5STUDIOHDR_FLAGS_STATIC_PROP
0x6STUDIOHDR_FLAGS_USES_FB_TEXTURE
0x7STUDIOHDR_FLAGS_HASSHADOWLOD
0x8STUDIOHDR_FLAGS_USES_BUMPMAPPING
0x9STUDIOHDR_FLAGS_USE_SHADOWLOD_MATERIALS
0xaSTUDIOHDR_FLAGS_OBSOLETE
0xbSTUDIOHDR_FLAGS_UNUSED
0xcSTUDIOHDR_FLAGS_NO_FORCED_FADE
0xdSTUDIOHDR_FLAGS_FORCE_PHONEME_CROSSFADE
0xeSTUDIOHDR_FLAGS_CONSTANT_DIRECTIONAL_LIGHT_DOT
0xfSTUDIOHDR_FLAGS_FLEXES_CONVERTED
0x10STUDIOHDR_FLAGS_BUILT_IN_PREVIEW_MODE
0x11STUDIOHDR_FLAGS_AMBIENT_BOOST
0x12STUDIOHDR_FLAGS_DO_NOT_CAST_SHADOWS
0x13STUDIOHDR_FLAGS_CAST_TEXTURE_SHADOWS
0x16STUDIOHDR_FLAGS_VERT_ANIM_FIXED_POINT_SCALE
numbones(int)<parsed from long>
boneindex(int)<parsed from long>

offset to an array of mstudiobone_t

numbonecontrollers(int)<parsed from long>
bonecontrollerindex(int)<parsed from long>

offset to an array of mstudiobonecontroller_t

numhitboxsets(int)<parsed from long>
hitboxsetindex(int)<parsed from long>

offset to an array of mstudiohitboxset_t

numlocalanim(int)<parsed from long>

local only; a model that gets its animations from an $includemodel may have 0 here

localanimindex(int)<parsed from long>

offset to an array of mstudioanimdesc_t

numlocalseq(int)<parsed from long>

local only; see numlocalanim

localseqindex(int)<parsed from long>

offset to an array of mstudioseqdesc_t

activitylistversion(int)<parsed from long>

runtime scratch; set when the sequences get indexed

eventsindexed(int)<parsed from long>

runtime scratch

numtextures(int)<parsed from long>
textureindex(int)<parsed from long>

offset to an array of mstudiotexture_t

numcdtextures(int)<parsed from long>

material search paths

cdtextureindex(int)<parsed from long>

offset to an array of int, each itself an offset to a string; both hops are relative to studiohdr_t

numskinref(int)<parsed from long>
numskinfamilies(int)<parsed from long>
skinindex(int)<parsed from long>

offset to a short[numskinfamilies][numskinref] replacement table

numbodyparts(int)<parsed from long>
bodypartindex(int)<parsed from long>

offset to an array of mstudiobodyparts_t

numlocalattachments(int)<parsed from long>
localattachmentindex(int)<parsed from long>

offset to an array of mstudioattachment_t

numlocalnodes(int)<parsed from long>
localnodeindex(int)<parsed from long>

offset to a byte[numlocalnodes*numlocalnodes] transition matrix

localnodenameindex(int)<parsed from long>

offset to an array of int, each itself an offset to a string

numflexdesc(int)<parsed from long>
flexdescindex(int)<parsed from long>

offset to an array of mstudioflexdesc_t

numflexcontrollers(int)<parsed from long>
flexcontrollerindex(int)<parsed from long>

offset to an array of mstudioflexcontroller_t

numflexrules(int)<parsed from long>
flexruleindex(int)<parsed from long>

offset to an array of mstudioflexrule_t

numikchains(int)<parsed from long>
ikchainindex(int)<parsed from long>

offset to an array of mstudioikchain_t

nummouths(int)<parsed from long>
mouthindex(int)<parsed from long>

offset to an array of mstudiomouth_t

numlocalposeparameters(int)<parsed from long>
localposeparamindex(int)<parsed from long>

offset to an array of mstudioposeparamdesc_t

surfacepropindex(int)<parsed from long>

offset to a string

keyvalueindex(int)<parsed from long>

offset to a string holding the $keyvalues block

keyvaluesize(int)<parsed from long>

0 means the model has no keyvalues

numlocalikautoplaylocks(int)<parsed from long>
localikautoplaylockindex(int)<parsed from long>

offset to an array of mstudioiklock_t

mass(float)<parsed from float>
contents(int)<parsed from long>

BSP contents flags

numincludemodels(int)<parsed from long>

$includemodel; the source of the animations this model borrows at runtime

includemodelindex(int)<parsed from long>

offset to an array of mstudiomodelgroup_t

virtualModel(int)<parsed from long>

runtime pointer; zero on disk

szanimblocknameindex(int)<parsed from long>

offset to a string; the name of the external .ani file

numanimblocks(int)<parsed from long>
animblockindex(int)<parsed from long>

offset to an array of mstudioanimblock_t

animblockModel(int)<parsed from long>

runtime pointer; zero on disk

bonetablebynameindex(int)<parsed from long>

offset to a byte[numbones] of bone indices, sorted by bone name

pVertexBase(int)<parsed from long>

runtime pointer; zero on disk

pIndexBase(int)<parsed from long>

runtime pointer; zero on disk

constdirectionallightdot(int)<parsed from unsigned char>

only meaningful when STUDIOHDR_FLAGS_CONSTANT_DIRECTIONAL_LIGHT_DOT is set

rootLOD(int)<parsed from unsigned char>
numAllowedRootLODs(int)<parsed from unsigned char>
unused(int[1])<parsed from unsigned char>

padding, balancing the three bytes above out to 4

unused4(int)<parsed from long>

held zeroframe cache data before version 47

numflexcontrollerui(int)<parsed from long>
flexcontrolleruiindex(int)<parsed from long>

offset to an array of mstudioflexcontrollerui_t

flVertAnimFixedPointScale(float)<parsed from float>

only meaningful when STUDIOHDR_FLAGS_VERT_ANIM_FIXED_POINT_SCALE is set; otherwise the engine uses 1/4096

unused3(int[1])<parsed from long>
studiohdr2index(int)<parsed from long>

offset to a studiohdr2_t; 0 means absent

unused2(int[1])<parsed from long>
struct valvemdl.structs.studiohdr.studiohdr2_t
numsrcbonetransform(int)<parsed from long>
srcbonetransformindex(int)<parsed from long>
illumpositionattachmentindex(int)<parsed from long>
flMaxEyeDeflection(float)<parsed from float>

cosine of the max eye deflection; 0.0 means unset, in which case the engine substitutes cos(30) = 0.866

linearboneindex(int)<parsed from long>

offset to a mstudiolinearbone_t

sznameindex(int)<parsed from long>

when non-zero, overrides studiohdr_t.name; exists because that field is a fixed 64 byte buffer

m_nBoneFlexDriverCount(int)<parsed from long>
m_nBoneFlexDriverIndex(int)<parsed from long>
reserved(int[56])<parsed from long>