Avatar game events
A game event is a fact a host game reports about an avatar: it died, it jumped, it took a hit. Every event has a default reaction that needs no authoring (a dead avatar’s lids close), and a creator can add or replace the reaction per avatar, per platform, or per event. One system covers the face, the eyes and the sounds, so a pallet never has a flat, SDK-style list of sound slots beside a separate table of face rules.
The vocabulary is fixed: an event is what happened; a reaction is what the avatar does; a rule is an authored reaction on an event, with an optional tier narrowing it; a channel is what a reaction touches (the face, the eyes, a sound); support is whether a platform can report the event at all.
Events
Section titled “Events”| Event | Tiers | Default reaction |
|---|---|---|
died | Face closed: lids shut and held, eyes still, no blink. Lasts until revived. | |
revived | Face live again. | |
dying | none | |
jumped | none | |
footstep | walk, run | none |
landedHard | none | |
damaged | small, big | none |
The defaults live in one place in Core, AvatarGameEventDefaults, and every host reads them from there rather than closing a dead avatar’s eyes in its own death code. A sound has no default: an avatar that declares none is silent.
ragdolled is decided as its own event, distinct from died (BONELAB and Garry’s Mod ragdoll living things), and is not in the enum until a host reports it. spawned, seated, talking, grabbed and afk are surveyed in the events draft and wait the same way.
Support
Section titled “Support”Yes a host reports it to DH today. Game the game has the event and DH does not listen yet. No the game has no such thing. Unchecked nobody has looked.
| Event | BONELAB | Garry’s Mod | MegaBonk | ULTRAKILL | Engine |
|---|---|---|---|---|---|
died | Yes DH NPCs (PuppetMaster’s dead flag, NpcPlayerRig.Die) and the player (HeadSFX.DeathVocal) | Game PlayerDeath, OnNPCKilled | Game PlayerHealth.PlayerDied | Game NewMovement.dead | No no death yet |
revived | Yes pooled NPC respawn, HeadSFX.RecoveryVocal | No respawns instead | No | No respawn is a checkpoint restart | No |
dying | Yes HeadSFX.DyingVocal | Unchecked | Unchecked | Unchecked | No |
jumped | Yes HeadSFX.JumpEffort | Unchecked | Unchecked | Game NewMovement.Jump | Game MovementSoundKind.Jump |
footstep | Yes FootstepSFX.PlayStep; the game picks walk or run | Unchecked | Unchecked | Unchecked | Game MovementSoundKind.Footstep, surface-keyed |
landedHard | Yes the avatar’s highFallOntoFeet slot | Unchecked | Unchecked | Unchecked | Game the hard Land tier |
damaged | Yes HeadSFX.SmallDamageVocal / BigDamageVocal; the game picks the tier | Game EntityTakeDamage | Game PlayerHealth.DamagePlayer | Game NewMovement.GetHurt | No |
“Yes” on BONELAB means the mod reports the event; whether each vocal fires on the local player in a real run is what the next run’s [Sound] lines answer.
Reactions are components
Section titled “Reactions are components”A reaction is a root dh.reaction component on the avatar, holding the actions run at its event. Actions specialized for events are called from it. That buys the whole override story for free: a reaction is keyed by its on and tier and inherits down the inherits chain by the one merge rule every component does, so a derived avatar stating a reaction of the same key replaces its actions whole, and "removed": true drops the inherited one. There is no separate replace-or-merge rule to learn.
{ "components": [ { "$type": "dh.reaction", "on": "jumped", "actions": [ { "$type": "dh.speakSound", "clips": ["sounds/jump.ogg"] } ] }, { "$type": "dh.reaction", "on": "footstep", "actions": [ { "$type": "dh.playSound", "clips": ["sounds/step-wood.ogg"] } ] }, { "$type": "dh.reaction", "on": "damaged", "tier": "small", "actions": [ { "$type": "dh.speakSound", "clips": ["sounds/bark-38780.mp3", "sounds/bark-spaniel-41366.mp3"] } ] }, { "$type": "dh.reaction", "on": "damaged", "tier": "big", "actions": [ { "$type": "dh.speakSound", "clips": ["sounds/growl-1.mp3", "sounds/growl-2.mp3", "sounds/growl-3.mp3", "sounds/growl-4.mp3", "sounds/growl-5.mp3"] } ] }, { "$type": "dh.reaction", "on": "dying", "actions": [ { "$type": "dh.speakSound", "clips": ["sounds/death.ogg"] } ] } ]}A derived avatar that keeps the doggy set but replaces the dying cry with a clip from another pallet, and adds a jump chirp beside the base’s jump:
{ "inherits": "io.mltn.avatars.taidum-base:taidum.dh-avatar", "components": [ { "$type": "dh.reaction", "on": "dying", "actions": [ { "$type": "dh.speakSound", "clips": ["dev.pitr:sounds/hiss.ogg"] } ] }, { "$type": "dh.reaction", "on": "jumped", "actions": [ { "$type": "dh.speakSound", "clips": ["io.mltn.avatars.taidum-base:sounds/jump.ogg", "dev.pitr:sounds/chirp-one-short.wav"] } ] } ]}After the chain resolves, dying speaks only the hiss, and jumped picks between the base’s jump.ogg and the chirp, because the derived list is the whole list. { "$type": "dh.reaction", "on": "died", "removed": true } drops that event’s inherited reaction.
| Field | Type | Description |
|---|---|---|
on | event token | Which event. Required. |
tier | tier token | Narrows the rule to one tier of the event. Within one avatar’s resolved rules the tier match wins over the untiered rule, which is the fallback for every tier. |
actions | list | The actions, in order. |
A rule on the same on and tier as an inherited one replaces its actions whole. A rule naming an unknown event or tier is a build error.
Channels
Section titled “Channels”Today’s actions are the two sound ones. The face and eye channels are the defaults above and the same blendshape weights and dh.eyes the avatar already states, which the compiler does not yet accept inside a reaction: a reaction ends on its own when its event does (died ends at revived), so an authored blend shape must be undone by the host, and no host does that yet. Whisker’s draft settled the shape before that lands: each channel has one writer (the eye look), and a reaction submits a request to it (block beats blend) rather than writing the shape itself, so blinking and a creator’s death shape never fight.
Sound operations
Section titled “Sound operations”Two actions, distinguished by where the sound comes from and what it drives.
dh.playSound
Section titled “dh.playSound”A generic sound from the avatar’s body. It comes from the avatar’s center (the event’s context) or, with bone, from a named bone: a humanoid role such as head or a custom bone in the rig. Never the mouth. A bell on a collar, a prop, a footstep is a play.
| Field | Type | Description |
|---|---|---|
clips | list of clip references | One is picked at random each time. A list of one always plays that one. |
bone | string | The bone the sound comes from; omitted, the avatar’s center. |
| Platform | Support |
|---|---|
| BONELAB | Yes footstep and landedHard through the SDK’s foot slots (the game’s own 3D sources); a play on a head event comes from the mouth, since the game has one source per event. NPC bodies: a one-shot at the bone or the hips. bone is honored on NPC bodies only. |
| Garry’s Mod, MegaBonk, ULTRAKILL, Project Zomboid, Minecraft | No not reported yet |
| Engine | No MovementAudio plays the engine’s own cue set; no per-avatar override yet |
dh.speakSound
Section titled “dh.speakSound”A sound from the avatar’s mouth: positioned at the head, and driving the mouth from the clip’s amplitude on a host that drives a mouth. Pain, effort and the dying cry are speak.
| Field | Type | Description |
|---|---|---|
clips | list of clip references | As above. |
mouth | bool | Whether the clip’s amplitude drives the mouth channel while it plays. Default true. |
| Platform | Support |
|---|---|
| BONELAB | Yes jumped, damaged, dying, died, revived through HeadSFX, the head’s own low-passed 3D source. mouth is read and reported; no BONELAB body drives a mouth yet. |
| Garry’s Mod, MegaBonk, ULTRAKILL, Project Zomboid, Minecraft | No not reported yet |
| Engine | No |
The mouth-drive hook is present in the shape (ReactionAction.Mouth). A host drives it through the shared mouth pipeline: Core’s MouthClipFeed hands the playing clip’s samples to the same analyzer a live voice uses, so a clip shapes real visemes where the avatar has them and opens the mouth or jaw where it does not.
Clip references
Section titled “Clip references”A clip is a stored audio file: .wav, .ogg, .mp3, .flac or .aif, which the compiler already packs as-is under an audio/* MIME. It is referenced like every other asset: a path from the folder the definition sits in, a leading / for the pallet root, or pallet:path into a declared dependency. The compiler rewrites local paths to pallet-root paths at build and checks that every clip exists, that a barcode’s pallet is in dependencies, and that the extension is audio. At load, every clip is qualified to pallet:path, so a host opens it from the pallet that owns it and never resolves anything itself.
Where rules live
Section titled “Where rules live”| Layer (least to most specific) | Where |
|---|---|
| 1. DH default | AvatarGameEventDefaults |
| 2. Avatar | dh.reaction components in the avatar’s chain, base first |
| 3. Avatar on one platform | components in the platform settings file, laid over the whole chain |
The platform file is the avatar’s path under platforms/<platform>/ (the platform’s alias or ID) with .jsonc in place of .dh-avatar, the same file that holds npcs. Its components list holds dh.reaction components, laid over the chain by the same rule, removals included; anything else in the list is dropped with a warning, and so is the file’s old patches list, with the rename named. Its bare paths are pallet-root paths (the compiler does not rewrite platform files).
{ "npcs": true, "components": [ { "$type": "dh.reaction", "on": "jumped", "actions": [ { "$type": "dh.speakSound", "clips": ["sounds/jump.ogg"] } ] } ]}User settings (a player turning a channel off) are a fourth layer outside the rules, per host: BONELAB’s Sounds preference, the DigitalHeavenAnim eye table. Whisker’s draft leans that way (a rule editor per host is later).
How BONELAB plays them
Section titled “How BONELAB plays them”BONELAB voices a rig from the Marrow SDK’s AudioVarianceData slots on SLZ.VRMK.Avatar, each a list of clips the game picks from at random with no volume or pitch of its own. The mod maps each resolved reaction onto a slot when the template is built (jumped to bigEffort, footstep/walk to footstepsWalk, footstep/run to footstepsJog, an untiered footstep to both, landedHard to highFallOntoFeet, damaged/small to smallPain, damaged/big to bigPain, dying to dying, died to dead, revived to recovery), decodes the clips through UnityWebRequestMultimedia from a copy in the workspace cache, and fills the slots as they arrive. After every switch it also writes the same clips onto the rig’s HeadSFX and FootstepSFX, logging what the game had copied, so a slot the game read before the clip arrived still plays. A DH NPC body that is no rig gets a one-shot on the body instead. The game’s own hooks (HeadSFX.JumpEffort, DeathVocal and the rest) are reported back as the DH event they stand for, which is how the player’s face now closes on death.
Inspecting what resolves
Section titled “Inspecting what resolves”dh reactions [pallet] prints, for every avatar in the workspace sources (or one pallet’s), what its reactions resolve to at load: the chain it was read from, then per event and tier the ordered actions with every clip as pallet:path, and the same again under each platform file beside the avatar that carries reactions. It runs the one reading every host runs (AvatarReactions.Of, then Overlay for a platform file) over the source chain, so what it prints is what BONELAB fills its slots from. Reads only; writes nothing.
$ dh reactions io.mltn.avatars.mltn-taidum --plainio.mltn.avatars.mltn-taidum:mltn-taidum.dh-avatar chain: io.mltn.avatars.taidum-base:taidum.dh-avatar -> io.mltn.avatars.mltn-taidum:mltn-taidum.dh-avatar footstep/walk 1. dh.playSound [io.mltn.avatars.taidum-base:sounds/step-wood.ogg] footstep/run 1. dh.playSound [io.mltn.avatars.taidum-base:sounds/step-wood.ogg] landedHard 1. dh.playSound [io.mltn.avatars.taidum-base:sounds/step-wood.ogg] jumped 1. dh.speakSound [io.mltn.avatars.taidum-base:sounds/chirp-one-short.wav] damaged/small 1. dh.speakSound [io.mltn.avatars.taidum-base:sounds/chirp-one-short.wav] damaged/big 1. dh.speakSound [io.mltn.avatars.taidum-base:sounds/hiss.ogg] dying 1. dh.speakSound [io.mltn.avatars.taidum-base:sounds/hiss.ogg] revived 1. dh.speakSound [io.mltn.avatars.taidum-base:sounds/purr.wav]Here the derived avatar replaced the base’s jumped, both damaged tiers and dying with its own clips and added revived; the footsteps came through from the base untouched.
Studio shows the same resolution in its Reactions tab for a selected avatar, with each action’s origin (the avatar itself, a base in the chain, or a platform file) and a play button on every clip; its reaction cards edit dh.reaction components in a definition or a platform file, and an audio file’s player lists every reaction that names it (see Studio). Each resolved action carries the file that wrote it (AvatarReaction.Origins), which is how the tab and AvatarReactionsView say where a clip came from without resolving anything a second way.
A clip that names a file its pallet does not hold is a build error whatever form the reference takes: a local path is checked by the object validator, and a pallet:path barcode by AvatarReactionsCheck against the dependency’s source, in dh build and dh validate alike. A barcode into a pallet the build has no source for is a warning, since nothing can be checked.
- Physics as an event source. A bell’s chain hitting something, a tail slapping a wall: a spring chain’s collision is an event the same rules can answer with a
dh.playSoundat the bone. Noted, not built. - Face and eye actions inside a reaction, undone when the event ends.
- The remaining surveyed events (
ragdolled,spawned,seated,talking,grabbed,afk), each added when a host reports it. - Per-op volume or pitch, if a host ever needs them; the SDK has none, and nothing here has asked for one.