The parameter protocol

avatar osc send VRCEmote 3 # /avatar/parameters/VRCEmote (int) avatar osc input Vertical 0.5 # /input/Vertical, reset to 0 on exit avatar osc monitor # print every update VRChat broadcasts
AddressDirectionWhat
/avatar/parameters/<name>bothExpression parameters: bool / int / float, typed by the avatar's OSC config.
/input/<axis|button>to VRChatLocomotion axes and buttons; axes reset to 0 when the sender exits.
/avatar/changebothAvatar switch (blueprint id); VRChat also emits it on every change.

avatar-osc handles /avatar/parameters/*, /input/* (axes/buttons, reset-to-zero), and /avatar/change, split into a pure codec (encode/decode rosc::OscMessage, fully unit-tested) and a thin non-blocking ParamClient UDP transport (send to VRChat :9000, poll :9001). query parses an avatar's OSCQuery config JSON (AvatarConfig: parameter names, OSC type tags, read/write access) offline.

This is distinct from the tracker OSC in avatar_input::osc — that carries /tracking/* transforms to drive a local rig, not avatar parameters. They never mix.

Parameter capture & the gesture cross-tab

avatar osc capture [--seconds N] [-o events.jsonl] records the parameter stream VRChat broadcasts and reduces it to the report that answers "what does my controller actually deliver?": per-parameter update counts and value ranges, plus the gesture cross-tab — for every GestureLeft/GestureRight value held, how often it was entered and the range its …Weight float covered. A missing row or a weight range stuck at 0.000..0.000 localizes a gesture bug to the input/binding side in one session; healthy rows put the FX layer at fault. Raw events are appended as JSON lines as they arrive, so a cut-short session keeps its data.

Both capture front-ends advertise themselves over OSCQuery (avatar_osc::oscquery, --no-advertise to opt out): a hand-built mDNS responder answers PTR _oscjson._tcp.local while a tiny HTTP responder serves the ?HOST_INFO handshake and a parameter tree exposing /avatar — which tells modern VRChat to route its parameter output to the advertised UDP port, whatever it is, removing the fixed-9001 assumption. Scope: same-host VRChat (the advertised address is 127.0.0.1). The same logic ships as a standalone, dependency-slim binary avatar-gesture-capture that cross-compiles to a double-clickable Windows .exe for the machine that actually runs VRChat.

Offline controller replay

avatar osc replay <events.jsonl> --controller FX.controller [--layer NAME] [--timeline] runs a captured parameter log through the controller's state machines offline — no Unity — and prints the state timeline each layer actually went through: visits, dwell times, and the blend-parameter range covered inside each state. Capture proves what VRChat delivered; replay proves what the controller did with it. Simulated semantics are the subset the generated controllers use (ordered Any-State + state transitions, m_CanTransitionToSelf, the six condition modes); crossfades are instantaneous and exit-time transitions are not modelled.

The analog-gesture daemon

avatar-osc-gestures is the "Vive advanced controls on any hardware" feature: read a controller's analog trigger/grip, map to a VRChat gesture (GestureLeft/Right int 0–7) plus an analog Gesture*Weight, and send via ParamClient. It is deliberately glam-free — it defines its own minimal AnalogSource rather than depending on avatar-input, so the cli graph stays out of glam.

  • Pure HandMapping — deadzone rescale (dz,1] → (0,1], Fist by default, optional grip gesture.
  • GestureFrame::updates_since change detection — only changed params re-sent.
  • GestureDaemon loop (tick/run/run_for) over a ParamSink.

On-device input is OpenXR (an AnalogSource adapter, pending); headless uses a deterministic DemoSource triangle sweep.

avatar osc send VRCEmote 3 avatar osc query path/to/avatar-osc-config.json avatar osc capture --seconds 60 -o events.jsonl avatar osc replay events.jsonl --controller FX.controller --timeline avatar osc gestures --seconds 10