Skip to content

AAS Bookkeeping

TL;DR: Each non-core, non-local, non-curve animator parameter on a CVRAvatar consumes bits from a 3200-bit sync budget — 32 for floats and ints, 1 for bools (packed eight per byte). Cross the cap and additional parameters are silently kept local. Some Advanced Avatar Settings UI types fan out into multiple animator parameter names with -r/-g/-b or -x/-y/-z suffixes — make sure your animator declares the suffixed names, not the machine name.

AvatarDefinitions declares the constants:

public const int AAS_MAX_SYNCED_BITS = 3200;
public const int AAS_FLOAT_BIT_USAGE = 32;
public const int AAS_INT_BIT_USAGE = 32;
public const int AAS_BOOL_BIT_USAGE = 1;
public const string LOCAL_PARAMETER_PREFIX = "#";
Parameter typeBits
Float32
Int32
Bool1 (packed 8 per byte)
Trigger0 — always local

Triggers are forced local in AvatarParam’s constructor:

isLocal |= type == AnimatorControllerParameterType.Trigger;

…so they never enter the budget.

AvatarAnimatorManager.CreateParameterDefinition is called once per animator parameter at avatar setup. The accounting happens inside:

var avatarParam = new AvatarParam(name, isControlledByCurve, type, isParamInAas);
if (avatarParam.IsSynced && AASBitUsage < 3200)
{
switch (type)
{
case Float: AASSyncedFloatCount++; AASBitUsage += 32; ...
case Int: AASSyncedIntCount++; AASBitUsage += 32; ...
case Bool: AASSyncedBoolCount++; AASBitUsage += 1; ...
}
}

IsSynced requires:

  • isValid — name not null/empty
  • !isLocal — name does not start with # and the parameter is not a trigger
  • !IsReadOnly — not core, not curve-controlled

When Setup() finishes, the manager rounds the bool count up to a byte:

AASSyncedByteCount = (int)Math.Ceiling((float)AASSyncedBoolCount / 8f);

The outbound caches (_aasOutboundCacheFloat/Int/Bool) are filled with the parameters’ default values at registration. Subsequent writes (through SetParameter_InternalSetFloat/Int/Bool_Animator) update the cached value and set AASParameterChangedSinceLastSync = true. Each sync tick GetAASParameterSyncData() packs the caches into:

public struct AvatarAdditionalSyncData
{
public float[] AdditionalParametersFloat;
public int[] AdditionalParametersInt;
public byte[] AdditionalParametersByte; // bool array packed 8/byte
}

AASParameterChangedSinceLastSync is only reset by GetAASParameterSyncData(), so dirty-then-flush is the actual cadence.

AASBitUsage < 3200 is checked before each parameter is added to the indices. If AASBitUsage is already at the cap when a new synced parameter is encountered:

  • the parameter is not added to _aasOutboundIndicesFloat/Int/Bool
  • the parameter is not added to _aasOutboundCacheFloat/Int/Bool
  • subsequent writes update the underlying Animator value normally
  • the value is never packed into an outbound sync packet

So crossing the cap silently demotes additional parameters to local-only. The AASBitUsage property is the running total — query it from a mod to estimate how close to the cap an avatar is.

Authors expose AAS controls in the inspector by populating CVRAdvancedAvatarSettings.settings. At avatar load, PlayerBase.PopulateAasParameterNameCache walks every CVRAdvancedSettingsEntry and pre-computes the set of expected animator parameter names from the entry’s machineName:

AAS typeAnimator parameter names produced
MaterialColor<machineName>-r, <machineName>-g, <machineName>-b
Joystick3D, InputVector3<machineName>-x, <machineName>-y, <machineName>-z
Joystick2D, InputVector2<machineName>-x, <machineName>-y
Everything else (Slider, Dropdown, Toggle, …)<machineName>

So a colour picker called HairColor requires HairColor-r, HairColor-g, and HairColor-b parameters on your animator (Float type). Declaring just HairColor won’t pick up the colour picker’s writes.

The names produced here are added to Avatar.AvatarSettingsParameterNames, which is consulted at parameter creation time:

bool isParamInAas = Avatar.AvatarSettingsParameterNames.Contains(name);

This is what makes a parameter IsAAS == true (and therefore eligible for profile saving).

AvatarAnimatorManager.GetAdditionalSettingsCurrent() returns the current values of every parameter where CanSaveToProfile is true:

public bool CanSaveToProfile => isValid && !isCore && !IsReadOnly && IsAAS;

So a parameter is profile-saveable only when:

  • it has a valid name
  • it isn’t in CoreParameters (so MovementX, Grounded, etc. are excluded)
  • it isn’t read-only (so curve-controlled params are excluded)
  • it has been declared in the AAS UI (isParamInAas == true)

That’s why ad-hoc animator parameters that you write to from a script but never expose in the AAS settings will never persist to a profile.

ApplyAdvancedSettingsFileProfile(values) is the inverse: it loops over the loaded CVRAdvancedSettingsFileProfileValue list, calls SetParameter for each, and notifies the menu via SendAdvancedAvatarUpdate(name, value, markChange: false).

AvatarAnimatorManager exposes the running state as public read-only properties:

PropertyMeaning
AASBitUsageTotal synced bits used. Capped at 3200.
AASSyncedFloatCountSynced float parameter count.
AASSyncedIntCountSynced int parameter count.
AASSyncedBoolCountSynced bool parameter count.
AASSyncedByteCountceil(AASSyncedBoolCount / 8).
AASParameterChangedSinceLastSyncDirty flag — set on any synced write, cleared by GetAASParameterSyncData().

Useful when building a tool that visualises how much of the 3200-bit budget your avatar is consuming.