What a .controller file is

A .controller is a multi-document Unity YAML stream: one AnimatorController object (class id 91) owns a graph of further objects, linked by local fileIDs.

Class idObjectWhat this crate reads from it
91AnimatorControllername, declared parameters (m_AnimatorParameters)
1107AnimatorStateMachinechild-state count, whether m_DefaultState resolves
1102AnimatorStatem_WriteDefaultValues and the m_Motion reference (one per state)
1101 / 1109AnimatorStateTransition / AnimatorTransitioneach m_Conditions entry
206BlendTreeblend type, blend parameter(s), per-child direct parameters + external child-motion guids

Reading a controller

use avatar_unity_asset::{AnimatorController, AnimationClip};
use avatar_unity_yaml::UnityFile;

let file = UnityFile::parse(&std::fs::read_to_string("Assets/FX.controller")?)?;
let ctrl = AnimatorController::from_file(&file).expect("no controller document");
for c in &ctrl.conditions {
    println!("{} mode={} threshold={}", c.parameter, c.mode, c.threshold);
}
let clip = AnimationClip::from_file(&UnityFile::parse(&std::fs::read_to_string("Assets/Smile.anim")?)?);
assert!(clip.map_or(true, |c| !c.animates_muscles()));

There is no CLI for this crate; avatar lint is its consumer.

Aggregate, not full graph

For the questions the lint rules ask — which parameters are referenced, are write-defaults consistent, is a default state set — the full linked graph is unnecessary. So from_file walks the file's documents and aggregates the relevant fields by Unity class id rather than rebuilding the state-machine topology. That is robust to SDK version drift, because the field names are stable Unity serialization rather than VRChat specifics.

Key controller types

  • AnimatorController — the parsed result: name, parameters, conditions (every condition across every transition), blend_trees, state_machines, write_defaults (one bool per state, in document order), states, state_count. from_file(&UnityFile) returns None if the file has no controller document.
  • AnimatorParameterraw_type is Unity's m_Type (1 Float, 3 Int, 4 Bool, 9 Trigger).
  • AnimatorConditionparameter, raw m_ConditionMode (1 If, 2 IfNot, 3 Greater, 4 Less, 6 Equals, 7 NotEqual), threshold (Unity's misspelled m_EventTreshold field).
  • BlendTreeInfoblend_type (0 = 1D, 1–3 = 2D variants, 4 = Direct); referenced_parameters() returns only what the tree actually reads given its type: a 1D tree reads X, a 2D tree X and Y, a Direct tree each child's direct parameter.
  • StateMachineInfohas_default_state is true only when m_DefaultState points at a real state.
  • StateInfo — per-state name, write-defaults flag, and a MotionRef { file_id, guid }: a local blend tree (fileID only), an external clip (guid set), or null. blend_tree_motion_guids collects every external guid blend-tree children reference.

AnimationClip (.anim, class 74)

AnimationClip::from_file reads a clip down to its curve bindings — what each curve animates, not the keyframe data, which is all the clip-content lint rules need:

  • float_curves — every m_FloatCurves entry as { path, attribute, class_id } (blendshapes bind class 137, GameObject toggles class 1, humanoid muscle curves class 95 with an empty path — is_muscle() tests the latter).
  • transform_curves — total entries across position/rotation/euler/scale curves; pptr_curvesm_PPtrCurves entries (material swaps).
  • Predicates: is_empty(), animates_transforms(), animates_muscles().

What lint asks of it

The controller and clip lint rules are direct consumers: undeclared parameters in transition conditions and blend trees (respecting blend type), state machines with no default state, duplicate parameter names, and mixed Write Defaults across states — a controller should be all-on or all-off. The clip bindings feed the clip-content rules (empty clips, transform or muscle curves where they don't belong).

Other typed asset graphs (descriptor, menus, parameters) are read elsewhere — see crates/vrc-descriptor/README.md. Material / scene typing is still to come.