Coop actions
Waypointed synced actions — anchor-local paths, the cinematic camera, the takedown.
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)action_events.json maps that event to a synced action id.synced_actions/<id>.json defines the action — one or more roles, each naming a
clip and (optionally) a waypoint path.This works on a dedicated server (clients resolve and render the clip locally).
One JSON file per action:
data/<namespace>/synced_actions/<id>.jsonThe 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.
| Field | Type | Required? | Notes |
|---|---|---|---|
duration_ticks | int ≥ 1 | required | Session 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_gate | double | required | Max start distance (blocks) for waypointed actions. Ignored for clip-only defs (no position driving means no proximity check). |
cinematic_camera | bool | optional (default false) | Behind-player cinematic for waypointed actions when no camera block is present. See Coop actions → Cinematic camera. |
camera | object {x,y,z} | optional | Anchor-local cinematic camera offset (doubles). Waypointed actions only. |
priority | string | optional (default MIDDLE) | One of LOWEST / LOW / MIDDLE / HIGH / HIGHEST. Informational only; it does not change where the clip renders. |
roles | list | required | 1+ roles. See below. |
Each entry in roles:
| Field | Type | Notes |
|---|---|---|
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). |
clip | string | A 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). |
waypoints | list (≥ 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. |
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 key | Fired when | Built-in default action → clip(s) |
|---|---|---|
tiedup:capture_grab | A kidnapper grabs / captures a target | capture_grab (2-role) → reaction.restrain.{initiator,target} |
tiedup:enslave | A captured target is enslaved | enslave (2-role) → reaction.restrain.{initiator,target} (same pair) |
tiedup:recapture | A fled target is recaptured | recapture (2-role) → reaction.restrain.{initiator,target} (same pair) |
tiedup:punish | A captor punishes a target | punish (2-role) → reaction.restrain.{initiator,target} (same pair) |
tiedup:hurt | A bound entity is hurt / shocked | hurt (self) → reaction.flinch |
tiedup:lockpick_fail | A lockpick attempt fails | lockpick_fail (self) → reaction.flinch (same flinch clip) |
tiedup:struggle | A struggle beat fires (start edge or shock-collar interrupt) | struggle (self) → reaction.struggle |
tiedup:leash_yank | A 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:death | A bound entity dies | death (self) → tiedup:death — a shipped self-role def whose clip id is an unauthored placeholder (resolves to the empty animation) |
action_events.json is rejected (skipped — check the log);
the rest of the table still loads.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).constructor
id — exactly like any pose clip. See Animations.data/<ns>/synced_actions/<id>.json with one role
per participant, each naming a clip id. Omit waypoints for a cosmetic one-shot.data/<ns>/tiedup/action_events.json — or, for a
waypointed coop action, wire your own trigger (see the coop keybind note on
Coop actions)./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.
Animations
Author the per-role clips: the constructor block, format, pose modifiers, events.
Item JSON
on_equip / on_unequip one-shots — the per-item flavour of a synced one-shot.
Planned & partial
The unauthored default clips and other honest limitations.