Skip to content

Synced actions & events

A synced action is the one unit a gameplay moment triggers to play a synchronized animation. When the server fires a capture grab, a death, a struggle, or a takedown, it looks up a synced action, then broadcasts the per-role clip to every viewer — including the entity it happened to — so everyone sees the same thing. The whole map of event → action → clip is datapack-authorable: no Java.

gameplay event ──action_events.json──▶ synced action id ──synced_actions/<id>.json──▶ per-role clip(s)
(closed enum) (datapack) (broadcast S→C)
  1. Server-authoritative gameplay code passes a gameplay event (a closed enum value).
  2. action_events.json maps that event to a synced action id.
  3. synced_actions/<id>.json defines the action — one or more roles, each naming a clip and (optionally) a waypoint path.
  4. The server branches by shape:
    • clip-only (no role has waypoints) → for each role, broadcast its clip to every tracking client and the entity itself. Cosmetic only.
    • waypointed (any role has waypoints) → engage the position driver that anchors and moves both entities along the path (this is a coop / takedown — see Coop actions), and also broadcast the clips.

This works on a dedicated server (clients resolve and render the clip locally).

Defining a synced action ✅ Available

Section titled “Defining a synced action ”

One JSON file per action:

data/<namespace>/synced_actions/<id>.json

The action id is the file name (without .json), keyed by its path: a file at data/mymod/synced_actions/struggle_kick.json registers the id struggle_kick. Actions reload on world load and on /reload, and are synced to clients.

The common case: a single self-clip that plays on the affected entity. This is exactly the shape of the built-in struggle, hurt, lockpick_fail, … defaults (the shipped struggle.json):

{
"duration_ticks": 1,
"proximity_gate": 0.0,
"priority": "MIDDLE",
"roles": [
{ "role": "self", "clip": "tiedup:reaction.struggle" }
]
}

A two-role clip-only action (e.g. an initiator + target restrain pose) lists two roles, neither with waypoints — the server fans out one broadcast per entity. This is the shape of the shipped capture_grab.json / enslave.json / recapture.json / punish.json, which all share the one reaction.restrain clip pair:

{
"duration_ticks": 1,
"proximity_gate": 0.0,
"priority": "HIGHEST",
"roles": [
{ "role": "initiator", "clip": "tiedup:reaction.restrain.initiator" },
{ "role": "target", "clip": "tiedup:reaction.restrain.target" }
]
}

Verified field-by-field.

FieldTypeRequired?Notes
duration_ticksint ≥ 1requiredSession length in server ticks (20 = 1 s). 1 is the conventional value for an instant clip-only one-shot; a waypointed action uses the real duration. Values ≤ 0 are rejected.
proximity_gatedoublerequiredMax start distance (blocks) for waypointed actions. Ignored for clip-only defs (no position driving means no proximity check).
cinematic_camerabooloptional (default false)Behind-player cinematic for waypointed actions when no camera block is present. See Coop actions → Cinematic camera.
cameraobject {x,y,z}optionalAnchor-local cinematic camera offset (doubles). Waypointed actions only.
prioritystringoptional (default MIDDLE)One of LOWEST / LOW / MIDDLE / HIGH / HIGHEST. Informational only; it does not change where the clip renders.
roleslistrequired1+ roles. See below.

Each entry in roles:

FieldTypeNotes
role"initiator" | "target" | "self"Lowercase. self and initiator both map to the initiating entity; target maps to the second entity (the one passed as the target). Use self for single-entity one-shots (death, hurt).
clipstringA full ResourceLocation (e.g. "mymod:grab_initiator") naming the per-role visual clip. Should match a registered clip’s constructor id for a visible pose — see Animations. An unmatched id is not rejected: it falls back to the empty animation (you’ll see a one-time warning in the log — see the caution below).
waypointslist (≥ 2)Optional. Omit (or leave empty) → the role is clip-only (cosmetic, no movement). Present → the role drives anchored position and engages the coop position driver; the full list of constraints is documented on Coop actions.

Binding an event ✅ Available

Section titled “Binding an event ”

Gameplay code does not name a synced action directly — it fires a gameplay event, and action_events.json maps events to action ids. One table file per namespace:

data/<namespace>/tiedup/action_events.json
{
"tiedup:capture_grab": "tiedup:capture_grab",
"tiedup:struggle": "mymod:struggle_kick",
"tiedup:hurt": "mymod:hurt_flinch"
}

The key is a gameplay event; the value is a synced-action id (resolved by its path against the registry). To re-skin a built-in event, point its key at your action id — e.g. bind tiedup:struggle to mymod:struggle_kick and the struggle one-shot plays your action instead.

✅ Available Event keys are a fixed, mod-defined enum — you cannot invent new gameplay events from a datapack (that would need Java to fire them). Every key is validated against this set, and any unknown key is rejected (skipped, with a warning in the log). These are the exact built-in keys (a fixed set):

Event keyFired whenBuilt-in default action → clip(s)
tiedup:capture_grabA kidnapper grabs / captures a targetcapture_grab (2-role) → reaction.restrain.{initiator,target}
tiedup:enslaveA captured target is enslavedenslave (2-role) → reaction.restrain.{initiator,target} (same pair)
tiedup:recaptureA fled target is recapturedrecapture (2-role) → reaction.restrain.{initiator,target} (same pair)
tiedup:punishA captor punishes a targetpunish (2-role) → reaction.restrain.{initiator,target} (same pair)
tiedup:hurtA bound entity is hurt / shockedhurt (self) → reaction.flinch
tiedup:lockpick_failA lockpick attempt failslockpick_fail (self) → reaction.flinch (same flinch clip)
tiedup:struggleA struggle beat fires (start edge or shock-collar interrupt)struggle (self) → reaction.struggle
tiedup:leash_yankA leash is yanked (edge-detected, fires once per pull)leash_yank (self) → tiedup:leash_yank — a shipped self-role def whose clip id is an unauthored placeholder (resolves to the empty animation)
tiedup:deathA bound entity diesdeath (self) → tiedup:death — a shipped self-role def whose clip id is an unauthored placeholder (resolves to the empty animation)
  • An unknown event key in action_events.json is rejected (skipped — check the log); the rest of the table still loads.
  • A binding whose action id has no def no-ops at trigger time (you’ll see a warning in the log).
  • A malformed synced_actions/*.json is skipped — check the log; the rest keep loading. If everything is empty or invalid, the built-in defaults stay in place (you never end up with no actions at all).

Authoring workflow ✅ Available

Section titled “Authoring workflow ”
  1. Author the clip(s) in Blender (EF-JSON addon) and register each with a constructor id — exactly like any pose clip. See Animations.
  2. Write the synced action at data/<ns>/synced_actions/<id>.json with one role per participant, each naming a clip id. Omit waypoints for a cosmetic one-shot.
  3. Bind it to a gameplay event in data/<ns>/tiedup/action_events.json — or, for a waypointed coop action, wire your own trigger (see the coop keybind note on Coop actions).
  4. /reload and trigger the event in-game. Watch the server log for skipped defs or rejected keys.

Coop actions

Waypointed synced actions — anchor-local paths, the cinematic camera, the takedown.

Coop actions

Animations

Author the per-role clips: the constructor block, format, pose modifiers, events.

Animations reference

Item JSON

on_equip / on_unequip one-shots — the per-item flavour of a synced one-shot.

Item JSON reference

Planned & partial

The unauthored default clips and other honest limitations.

Planned & partial