Animation trees (.atr)

Status and authority

This page specifies the animation tree format loaded by stock Call of Duty: United Offensive. The authority is the recovered stock parser and runtime in coduomp:

  • src/scripting/script_anim_runtime.c owns the parser and the tree registry;

  • src/animation/xanim_tree.c builds the compact runtime tree;

  • src/animation/xanim_tree_queries.c exposes queries over that tree;

  • src/scripting/script_compile_load.c and src/scripting/script_code_emit.c resolve % references and emit animation values;

  • src/client/cgame/animation/anim_tree_load.c and src/bg/bg_animation_tree_lookup.c load and look up trees.

The format is a naming and grouping format. It does not store animation curves, frame data, or notetrack content. A leaf points at an xanim asset; the asset holds the animation itself.

What a tree does

A tree gives scripts stable animation names. A script writes %run, and the compiler resolves run in the tree selected by #using_animtree. The reference becomes a packed animation value containing the tree registry index and the node index.

A tree also groups animations:

  • a parent can group a body region, a weapon set, or a pose family;

  • a parent can declare that its children play in sync;

  • a parent can force its descendants to survive tree pruning.

The tree is not a state machine. It has no transitions, conditions, or timers. The player animation script and the AI scripts add those layers.

File location and selection

The loader forms the path animtrees/<name>.atr. The name comes from a #using_animtree directive:

#using_animtree("generic_human");

The name must be a single identifier. The stock parser accepts letters, digits, and underscores, and rejects a slash, dot, or hyphen in this position. The loader canonicalizes the name like a script filename, so case is not significant.

Tree selection is compiler state. Each new source file starts without a selected tree. A #using_animtree directive selects a tree for the remainder of that file. A later directive in the same file can select another tree, and references after it resolve against the new tree. A % literal or an #animtree expression before any directive is a compile error.

The engine also loads trees directly by name. For example, the player animation layer looks up the multiplayer tree, and a turret script can load a narrow tree such as mg42 or 30cal.

Syntax

The tree file contains a sequence of nodes. It has no outer pair of braces.

tree       = { node } ;
node       = name
           | name "{" { node } "}"
           | name ":" properties "{" { node } "}" ;
properties = property { property } ;
property   = "loopsync" | "nonloopsync" | "complete" ;

A leaf is a bare name:

walk
run
strafe_left

A parent is a name followed by a braced body:

main
{
    torso
    {
        stand_shoot
        stand_idle
    }
    legs
    {
        walk_loop
        run_loop
    }
}

A parent with properties uses a colon before the body:

aim : nonloopsync
{
    aim_left
    aim_center
    aim_right
}

The colon must be a separate token, and the property list runs to the end of the line. The opening and closing braces must each be followed only by the end of the line. These rules make the format line-oriented even though it uses braces.

Names are identifiers. A duplicate name in one parent is a parse error:

duplicate animation

A name may appear under different parents because each parent has its own name table. The parser accepts that, but the tree resolver binds a name once per tree. If a script references a name that occurs more than once, the first node binds the reference and a later node reports duplicate animation '%s' in 'animtrees/%s.atr' during tree build. Keep names unique across a tree that scripts reference by name.

Comments

Both comment forms from the shared parser are accepted:

// a line comment

/* a block comment
   over several lines */

Stock files use both forms. generic_human.atr contains a block comment around a disabled sync group.

Node kinds

A leaf is an animation asset reference. The builder resolves it with FS_GetDataForFile("xanim", name, ""). If the asset is missing, the load fails with:

Cannot find 'xanim/%s'

A parent is a group. It may contain leaves, other parents, or both. The runtime keeps a child count and a first-child index for each parent. The root of the compact tree is a synthetic node at index 0; the file’s top-level nodes are its children.

A parent is not an animation asset. It has no leaf asset, and XAnimGetLength is not valid for it. A sync parent still reports time through XAnimHasTime. XAnimGetAnimName returns <non-leaf anim> for a parent that is asked for an animation name.

The parser also accepts an alias form at the syntax level, but the stock implementation rejects it:

FIXME: aliases not yet implemented

Do not use aliases in stock content.

Node properties

Property

Effect

Applies to

loopsync

Marks the parent as a synchronous group whose leaves loop. The parent becomes a sync node and its children are played in phase.

Parent with a body.

nonloopsync

Marks the parent as a synchronous group whose leaves do not loop. The parent becomes a sync node and its children are played in phase.

Parent with a body.

complete

Keeps the parent’s whole subtree when the parent is referenced by a script. It has no effect on playback.

Parent with a body.

A property on a leaf is an error:

properties cannot be applied to primitive animations

Properties combine on one line:

proneMG42_fire : complete loopsync
{
    proneMG42_fire_15down
    {
        // five body poses
    }
    proneMG42_fire_level
    {
        // ...
    }
    proneMG42_fire_15up
    {
        // ...
    }
}

complete and the sync bits are separate flags. complete is bit 3, loopsync is bit 0, and nonloopsync is bit 1 in the recovered property encoding.

Do not put both sync properties on one parent. The parser records both bits, and the sync setup rejects the node as both looping and non-looping.

Sync groups

A sync group makes several animations share a playback phase. The classic use is a set of aim or fire poses that differ by direction but must switch without drifting out of step.

The sync rules are enforced when the compact tree is built:

  • every leaf under a loopsync parent must itself be a looping asset;

  • every leaf under a nonloopsync parent must be a non-looping asset;

  • a sync group may not contain another sync group;

  • a sync node may not be declared twice in the same subtree.

A mismatch is fatal. The stock diagnostic is:

animation cannot be sync looping and sync nonlooping

A nested sync group is also fatal, reported as a duplicate sync specification.

The practical rule is to keep looping and non-looping variants in separate parents. generic_human.atr contains a comment that a set of animations was moved into a separate group because a single sync group mixed the two.

Pruning and placeholders

The stock loader does not keep every node in the file. It keeps the nodes that scripts actually reference, plus the parts of the tree those references require.

The sequence is:

  1. The script compiler records each %name reference against the selected tree.

  2. When the tree is loaded, the parser marks each node whose name is not in that reference set as missingFromTree.

  3. The builder removes marked nodes unless an ancestor forces them to stay.

  4. complete on a referenced parent forces the whole subtree to stay.

  5. A referenced parent whose children were all removed receives a synthetic void child. A referenced loopsync parent receives void_loop.

  6. An empty root tree also receives void.

This is why a sync group often carries complete. A script may reference only one aim direction, but the sync group needs its siblings to keep phase. Without complete, the unreferenced siblings can be pruned and the group loses the set it was built to synchronize.

The behavior is controlled by com_animCheck. The stock default is 0. Setting it to a nonzero value keeps every node, including nodes no script references. Use it to inspect a full tree or to expose a missing xanim asset that pruning would otherwise hide. Do not ship content that depends on the debugging value.

Runtime representation

The compact tree is an XAnim:

typedef struct {
    const char *name;
    uint32_t nodeCount;
    XAnimEntry entries[];
} XAnim;

Each XAnimEntry is either a leaf or a parent:

  • a leaf has a resolved animation asset pointer;

  • a parent has a child count, a first-child index, and property flags;

  • every entry has a parent index.

The root is index 0. The first child of the root is index 1. A parent’s children occupy a contiguous range starting at firstChildIndex.

The builder walks the parsed tree, stamps parent indexes, resolves leaves, and then runs the sync setup over the result. The runtime queries are small:

Query

Result

XAnimIsPrimitive

True for a leaf, false for a parent.

XAnimHasTime

True for a leaf or for a parent with a sync property.

XAnimIsLooped

True for a looping leaf asset or a looping sync parent.

XAnimGetAnimName

The leaf asset name, or <non-leaf anim>.

XAnimGetLength

The leaf asset length in seconds. Valid only for a primitive node; the function reads leafAsset unconditionally, so a parent index is not a legal argument.

XAnimGetNumChildren

The child count for a parent.

The tree itself stores no frame data. Every leaf holds a reference to the loaded xanim asset.

Script integration

A script selects a tree with a directive before its first animation reference:

#using_animtree("generic_human");

It then writes animation literals with %:

self setAnimKnob(%stand_idle, 1, 0.2);

The compiler resolves the name in the tree selected at that point in the file and emits a packed scr_anim_t value. The packing is:

(treeIndex << 16) | nodeIndex

The low 16 bits hold the node index. The high 16 bits hold the tree registry index. The registry index is nonzero; zero is reserved. This makes a reference to the root of the first registered tree pack as 0x00010000.

#animtree emits the current tree’s integer registry index. AI turret scripts use it to switch an entity to a narrow tree:

self UseAnimTree(#animtree);

The runtime keeps a table of loaded trees. Scr_FindAnimTree returns the loaded XAnim for a name, or NULL. Scr_FindAnim records a reference to a name in a named tree. The runtime reports a missing name when it builds the tree. The stock diagnostics are:

animation '%s' not defined in anim tree '%s'
duplicate animation '%s' in 'animtrees/%s.atr'

The first is a name that no node in the tree defines. Scr_CheckAnimsDefined raises it after Scr_CreateAnimationTree finishes. The second is a name that more than one node defines and a script references: the first node binds the reference, and a later node finds it already bound.

Limits

Limit

Value and consequence

Tree name length

49 bytes. The path buffer is MAX_QPATH (64 bytes), and the loader adds animtrees/ and .atr. A longer name fails with animation tree path is too long.

Tree registry

127 registered names per active registration slot. The runtime keeps two slots. Overflow reports maximum animation tree count exceeded (%i).

Node pool

XANIM_POOL_NODE_COUNT is 2048. This is an engine pool constant, not a per-file syntax rule.

Node name

A node name must fit the shared identifier and path limits. A leaf name is also an xanim asset name.

Property line

The property list must fit on one line. The parser does not continue a property list across lines.

A tree is loaded once and reused by its registry index. Changing the file while the game is running does not update an already loaded tree.

Diagnostics

Stage

Message

Meaning

Parse

duplicate animation

The same name appears twice in one parent.

Parse

properties cannot be applied to primitive animations

A leaf has a colon property list.

Parse

FIXME: aliases not yet implemented

The file uses the unsupported alias form.

Build

Cannot find 'xanim/%s'

A leaf names an asset that is not present.

Build

animation '%s' in '%s' cannot be sync looping and nonlooping

A leaf under a loopsync parent is a non-looping asset.

Build

animation '%s' in '%s' cannot be sync nonlooping and looping

A leaf under a nonloopsync parent is a looping asset.

Build

animation cannot be sync looping and sync nonlooping

The parent declares both sync properties.

Build

duplicate specification of animation sync in '%s', %d nodes above '%s'

A sync group is nested inside another sync group. The message names the tree, the depth of the inner node, and the leaf it reached.

Build

animation '%s' not defined in anim tree '%s'

A % reference names a node the selected tree does not define.

Build

duplicate animation '%s' in 'animtrees/%s.atr'

A % reference names a node that more than one node in the tree defines.

Script compile

bad anim tree name

The #using_animtree argument is not an identifier.

Script compile

#using_animtree was not specified

A % or #animtree appears before a tree directive.

Script compile

cannot reference animation from /# ... #/ comment

A % reference appears inside a developer-only block.

Load

animation tree path is too long

The tree name does not fit the animtrees/<name>.atr path.

Load

maximum animation tree count exceeded (%i)

The tree registry is full.

Examples

Minimal root leaves

stalingrad_runner.atr is a single root-level leaf:

stalingrad_flagrunner_idle

This tree has no parent groups. A script can reference the leaf by name.

Named groups

bomber_anim.atr uses root-level groups for crew roles:

pilot_anims
{
    // ...
}

copilot_anims
{
    // ...
}

wirelessop_anims
{
    // ...
}

The group names are organizational. Scripts reference the leaves inside them, not the group path.

Sync group

trenches_truck_dummies.atr contains:

walk_run : loopsync
{
    // looping walk and run leaves
}

generic_human.atr contains both loop modes for the same pose family:

stand_shoot : nonloopsync
{
    // ...
}

stand_shoot_auto : loopsync
{
    // ...
}

The two groups are separate because the leaves have different loop states.

Complete sync group

multiplayer.atr contains:

proneMG42_fire : complete loopsync
{
    proneMG42_fire_15down
    {
        // five body poses
    }
    proneMG42_fire_level
    {
        // ...
    }
    proneMG42_fire_15up
    {
        // ...
    }
}

complete keeps the three fire pose groups when the parent is referenced. The sync property keeps their leaves in phase.

Empty group

generic_human.atr contains an empty group:

testmap_testing
{
}

An empty group on its own is valid syntax. If a script references it, the loader inserts a void child so the parent is not empty. Empty groups that no script references are pruned.

Authoring checklist

  1. Confirm that every leaf has an xanim asset with the same logical name.

  2. Keep every script-referenced name unique across the tree. The parser only rejects duplicates within one parent.

  3. Put looping and non-looping variants in separate sync groups.

  4. Add complete to a sync group when scripts reference only some of its children.

  5. Reference the tree with a single identifier in #using_animtree.

  6. Build with com_animCheck 1 once to expose unreferenced missing assets.

  7. Return com_animCheck to 0 before shipping.

  8. Recheck the player animation table and AI scripts after renaming a node. A rename breaks every % reference that used the old name.

Implementation basis

Source

Role

src/scripting/script_anim_runtime.c

Tree parser, property flags, registry, pruning, script reference collection.

src/scripting/script_anim.h

Animation tree and reference types.

src/animation/xanim_tree.c

Compact tree builder, leaf resolution, parent indexes, sync setup.

src/animation/xanim_tree_queries.c

Primitive, time, loop, length, child, and name queries.

src/qcommon/xanim_types.h

XAnim and XAnimEntry layout.

src/qcommon/script_runtime_types.h

scr_anim_t packing and animation registry constants.

src/scripting/script_compile_load.c

#using_animtree handling, identifier validation, and tree lookup.

src/scripting/script_code_emit.c

% and #animtree emission.

src/qcommon/com_parse.c

Shared comment and token handling.

src/client/cgame/animation/anim_tree_load.c

Client tree load entry.

src/bg/bg_animation_tree_lookup.c

Game-side tree lookup.

animtrees/generic_human.atr

Stock sync, complete, comment, and empty-group examples.

animtrees/multiplayer.atr

Stock player tree with complete loopsync and complete nonloopsync groups.

animtrees/bomber_anim.atr

Stock named root groups.

animtrees/trenches_truck_dummies.atr

Stock loopsync group.

animtrees/stalingrad_runner.atr

Stock single-leaf tree.