Sequences

A sequence is what the game actually plays. studiohdr_t.localseqindex reaches an array of mstudioseqdesc_t, and each one names the animations it is built from, how to blend them, what events fire during it, and what it is allowed to transition to. Animations are the raw material; sequences are the thing an entity asks for by name.

Sequences reference animations through a grid. animindexindex points at a short[groupsize[1]][groupsize[0]] of animation indices, and the two paramindex values name the pose parameters that pick a cell. A one dimensional blend has groupsize of {n, 1}; a two dimensional one – a movement sequence blended over speed and direction, say – fills the grid. The SDK’s anim(x, y) clamps both axes rather than trusting the caller.

paramindex needs two caveats before it means anything. -1 marks an axis as unused, but a groupsize of 0 means there is no axis at all, and in that case studiomdl leaves paramindex at 0 rather than -1 – a value that looks like “pose parameter 0” and is not. More importantly, the index is into the model’s merged pose parameter list, not its local one. A model that borrows from another via $includemodel can name a pose parameter it does not itself declare: the nine c_*_arms viewmodels in Team Fortress 2 each have numlocalposeparameters == 0 and sequences pointing at parameters that live in c_*_animations.mdl. The SDK resolves this through GetSharedPoseParameter and the virtual model. ValveMDL does not build virtual models, so on a model with includes these indices are simply not ours to interpret.

posekeyindex has a bug in the SDK, and the bug is the format. The accessor is:

inline float *pPoseKey( int iParam, int iAnim ) const
    { return (float *)(((byte *)this) + posekeyindex) + iParam * groupsize[0] + iAnim; }

iParam * groupsize[0] uses the first axis’s stride for both axes, which is only right when iParam is 0. studiomdl writes what this reads, so a writer that “fixes” the stride produces a file the engine misreads. The comment two lines up admits the shape is wrong: “FIXME: make this 2D instead of 2x1D arrays”.

weightlistindex is a float per bone, letting a sequence apply to only part of the skeleton – an upper body gesture over a running lower body.

Events are cues, not effects. Each mstudioevent_t names a cycle and a thing to do at it: play a sound, spawn a particle, land a footstep. NEW_EVENT_STYLE (1<<10) in type distinguishes events named by the string at szeventindex from older ones identified by the event integer. options is an inline 64 byte buffer rather than an offset.

numikrules here is a count only. The rules themselves hang off the animdescs, not the sequence. 21.85% of animdescs have some.

struct valvemdl.structs.seq.mstudioseqdesc_t
baseptr(int)<parsed from long>

NEGATIVE offset back to the studiohdr_t

szlabelindex(int)<parsed from long>

offset to a string; the name the game asks for

szactivitynameindex(int)<parsed from long>

offset to a string

flags(int)<parsed from long>

looping and delta flags; the STUDIO_LOOPING family

activity(int)<parsed from long>

runtime scratch; resolved to a game dll value at load

actweight(int)<parsed from long>
numevents(int)<parsed from long>
eventindex(int)<parsed from long>

offset to an array of mstudioevent_t

bbmin(Vector)
bbmax(Vector)
numblends(int)<parsed from long>
animindexindex(int)<parsed from long>

offset to a short[groupsize[1]][groupsize[0]] grid of animation indices

movementindex(int)<parsed from long>

offset to a float per blend

groupsize(int[2])<parsed from long>

the two dimensions of the blend grid

paramindex(int[2])<parsed from long>

the pose parameters that index the grid

paramstart(float[2])<parsed from float>

local starting value, 0..1

paramend(float[2])<parsed from float>

local ending value, 0..1

paramparent(int)<parsed from long>
fadeintime(float)<parsed from float>

ideal cross fade in time; 0.2 by default

fadeouttime(float)<parsed from float>

ideal cross fade out time; 0.2 by default

localentrynode(int)<parsed from long>

transition node at entry

localexitnode(int)<parsed from long>

transition node at exit

nodeflags(int)<parsed from long>

transition rules

entryphase(float)<parsed from float>

used to match the entry gait

exitphase(float)<parsed from float>

used to match the exit gait

lastframe(float)<parsed from float>

the frame that should generate EndOfSequence

nextseq(int)<parsed from long>

auto advancing sequences

pose(int)<parsed from long>

index of the delta animation between end and nextseq

numikrules(int)<parsed from long>

a count only; the rules live on the animdescs

numautolayers(int)<parsed from long>
autolayerindex(int)<parsed from long>

offset to an array of mstudioautolayer_t

weightlistindex(int)<parsed from long>

offset to a float[numbones]; how much this sequence affects each bone

posekeyindex(int)<parsed from long>

offset to a float[2][groupsize[n]]; see the note about pPoseKey

numiklocks(int)<parsed from long>
iklockindex(int)<parsed from long>

offset to an array of mstudioiklock_t

keyvalueindex(int)<parsed from long>

offset to a string

keyvaluesize(int)<parsed from long>

0 means no keyvalues

cycleposeindex(int)<parsed from long>

offset to a local pose parameter used to cycle through animations

activitymodifierindex(int)<parsed from long>

offset to an array of mstudioactivitymodifier_t

numactivitymodifiers(int)<parsed from long>
unused(int[5])<parsed from long>
struct valvemdl.structs.seq.mstudioactivitymodifier_t
sznameindex(int)<parsed from long>

offset to a string

struct valvemdl.structs.seq.mstudioiklock_t
chain(int)<parsed from long>
flPosWeight(float)<parsed from float>
flLocalQWeight(float)<parsed from float>
flags(int)<parsed from long>
unused(int[4])<parsed from long>
struct valvemdl.structs.seq.mstudioautolayer_t
iSequence(int)<parsed from short>
iPose(int)<parsed from short>
flags(int)<parsed from long>

the STUDIO_AL family

start(float)<parsed from float>
peak(float)<parsed from float>
tail(float)<parsed from float>
end(float)<parsed from float>
struct valvemdl.structs.seq.mstudioevent_t
cycle(float)<parsed from float>

when in the sequence this fires, 0..1

event(int)<parsed from long>

the legacy numeric event id

type(int)<parsed from long>

NEW_EVENT_STYLE (1<<10) means the event is named by szeventindex instead

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

stored inline as a fixed 64 byte buffer, not as an offset

szeventindex(int)<parsed from long>

offset to a string