"Citizen animgraph combat params are unwrapped and unnetworked"
▸ SYMPTOM
Combat animations (attack swings, weapon raises, combat roll/slide) either don't play at all, or play on the local peer but are invisible to other players. CitizenAnimationHelper has no methods for triggering attacks or setting combat-specific states.
▸ CAUSE
The stock citizen animgraph (citizen.vanmgrph) ships combat parameters that CitizenAnimationHelper does NOT wrap. The helper only exposes locomotion + holdtype/b_jump/b_deploy. Combat params exist in the animgraph but have no helper method -- you must drive them by name via renderer.Set(...).
The engine PlayerController (UseAnimatorControls) replicates ONLY locomotion, so combat params show on the local peer but NOT on proxies unless you replicate them yourself.
▸ FIX
Drive the combat params directly and replicate them:
// Available combat params (verified from citizen.vanmgrph):
renderer.Set("b_attack", true); // one-shot trigger (auto-resets)
renderer.Set("holdtype_attack", 0f); // attack variant (0-8, cycle for combos)
renderer.Set("holdtype", (int)holdType); // HoldTypes enum (Punch=5, Swing=6, etc.)
renderer.Set("special_movement_states", (int)moveStyle); // SpecialMoveStyle (Roll=2, Slide=3)
renderer.Set("b_weapon_lower", false); // false = weapon raised (no dedicated guard pose)Param reference:
| Param | Type | Behavior |
|---|---|---|
b_attack | bool (auto-reset) | One-shot swing/fire trigger -- set true, it self-clears |
holdtype_attack | float (0-8) | Attack variant clip for current holdtype -- cycle 0/1/2 for combos |
holdtype | int | HoldTypes enum: None=0, Pistol=1, Rifle=2, Shotgun=3, HoldItem=4, Punch=5, Swing=6, RPG=7, Physgun=8 |
special_movement_states | int | SpecialMoveStyle: None=0, LedgeGrab=1, Roll=2, Slide=3 -- HELD state, not a trigger |
b_weapon_lower | bool | Keep false for weapon raised; no dedicated block/guard pose exists |
Networking: push persistent state (holdtype, roll) via a [Sync] field applied on every peer each frame, and fire discrete triggers (b_attack) via a host-side [Rpc.Broadcast]:
[Sync] public int SyncedHoldType { get; set; }
[Sync] public int SyncedMoveStyle { get; set; }
// On every peer, every frame:
renderer.Set("holdtype", SyncedHoldType);
renderer.Set("special_movement_states", SyncedMoveStyle);
// Discrete triggers via broadcast:
[Rpc.Broadcast]
void BroadcastAttack(float variant)
{
renderer.Set("holdtype_attack", variant);
renderer.Set("b_attack", true);
}Because play mode always creates a lobby, the host is a peer and receives its own broadcast, so single-player still animates correctly.
Fanning out a cosmetic one-shot: a Sync counter beats a broadcast
For a discrete cosmetic one-shot on top of the animgraph (a jump flourish, a trick trigger), a [Sync] counter is more robust than an [Rpc.Broadcast] or a replicated timestamp. Increment a small owner-side counter on the trigger event, edge-detect the change on every peer (including the owner) each frame, and drive a local ease timer per peer off that edge, rather than replicating the animation state itself:
[Sync] public int FlourishCounter { get; set; }
int _lastSeen;
bool _seeded;
protected override void OnUpdate()
{
if ( !_seeded ) { _lastSeen = FlourishCounter; _seeded = true; return; } // seed-guard on join
if ( FlourishCounter != _lastSeen )
{
_lastSeen = FlourishCounter;
StartLocalFlourishTimer(); // per-peer local reaction
}
}
// Owner, on the trigger:
if ( Networking.IsOwner ) FlourishCounter++;A counter sidesteps two broadcast/timestamp failure modes: cross-machine clock skew can't misalign a plain increment the way it can a timestamp-driven timer, and a rapid re-trigger still reads as a fresh edge every time (a broadcast can coalesce, a timestamp can tie). Seed-guard the first observed value per peer (store it on first read and only fire on a change from that baseline) so a client joining mid-session doesn't read the counter's already-nonzero value as a fresh edge and spuriously play the one-shot on spawn.
Pick the fan-out shape by event type: Rpc.Broadcast for a true fire-and-forget one-shot effect; the Sync-counter-edge for a per-peer local animation or timer reaction. Either way, the animgraph / CitizenAnimationHelper only auto-replicates locomotion (never custom one-shot visual state) so every discrete cosmetic trigger needs one of these two shapes.
Reading the full param surface offline
Every citizen animgraph parameter name is readable offline, straight from citizen.vanmgrph with a plain m_name regex, no live editor, no MCP, no running engine. The file is KV3 text and every parameter node carries its name in an m_name field, so a static text/regex scan lists the entire parameter surface in one pass. CitizenAnimationHelper wraps most of what that scan turns up; the notable documented gap is b_attack (above), which the helper doesn't surface, confirmed still true on engine 26.07.22. Prefer the offline read over any "ask the running editor what params exist" workflow: it's faster and needs nothing booted.
▸ WHY IT WORKS
The animgraph params exist and are fully functional -- they just lack a C# wrapper in CitizenAnimationHelper. Driving them by name with renderer.Set talks directly to the animgraph. The per-peer sync approach mirrors the engine's own locomotion replication pattern but extends it to the combat params the engine doesn't cover.
- Added the offline param-surface read (m_name regex over citizen.vanmgrph); re-verified b_attack gap on engine 26.07.22
- Added the cosmetic one-shot fan-out pattern (Sync-counter edge vs Rpc.Broadcast)
- Published