Skip to content

Coop actions

A coop action choreographs two entities at once — an initiator and a target — along authored, anchor-local motion paths, with an optional cinematic camera. The shipped example is the takedown: aim at a bondage-eligible entity, press a key, and the target is pulled in and tackled while both bodies follow scripted curves and the camera frames the scene.

The whole thing is datapack-authorable: one JSON file per action defines duration, the proximity gate, both roles’ waypoint paths, and the camera. The two visual poses are a pair of EF-JSON clips (authored exactly like any other animation).

How it runs ✅ Available

Section titled “How it runs ”

The lifecycle is server-authoritative and wired end-to-end:

  1. Trigger. The initiator aims their crosshair at a LivingEntity and presses the V key (default, rebindable under key.categories.tiedup). A request packet is sent to the server with the target’s id and the action id (takedown).
  2. Gate + start. The server looks up the action, checks the target is bondage-eligible, checks the distance against proximity_gate, reserves both entities atomically, snapshots the anchor (the initiator’s position + yaw at start), snaps both participants to t=0, and — only on a successful gate — broadcasts each role’s clip and the position lock to all viewers.
  3. Drive. Every server tick, the server samples each role’s waypoint curve in anchor-local space and moves both participants’ authoritative position along it. Motion is 100% waypoint-driven — the server never reads animation clip data.
  4. Mirror. Clients snap the local participant to the identical computed path (no rubber-band), suppress its movement input, render the per-role clip for that participant, and — for a cinematic action — drive the camera.
  5. End. When elapsed >= duration_ticks, the session ends and control returns to both entities. Lost packets, death, disconnect, or dimension change abort cleanly; late joiners re-sync.

A coop action is a single JSON file, in the canonical synced-actions directory:

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

The action id is the file name (without .json). data/tiedup/synced_actions/takedown.json → the action id takedown. The loader is a server data-reload listener, so actions reload on world load and on /reload, and are synced to clients on login and on /reload.

Verified field-by-field against the parser. Top-level fields:

FieldTypeRequired?Notes
duration_ticksint ≥ 1requiredTotal session length in server ticks (20 = 1 s). Values ≤ 0 are rejected at load.
proximity_gatedoublerequiredMax start distance (blocks) between initiator and target. Farther → the start is silently refused.
cinematic_camerabooloptional (default false)Enables the cheap behind-player cinematic when no camera block is present. See Cinematic camera.
cameraobject {x,y,z}optionalAnchor-local cinematic camera offset (doubles). See Cinematic camera.
prioritystringoptional (default MIDDLE)LOWEST/LOW/MIDDLE/HIGH/HIGHEST — informational only; it does not change where the clip renders. Shared with all synced actions.
roleslistrequiredFor a coop (waypointed) action: both an initiator and a target, each with a waypoints path. A waypointed action missing a role is rejected at load. (Clip-only synced actions relax this — see Synced actions.)

Each entry in roles:

FieldTypeNotes
role"initiator" | "target" | "self"Lowercase string in JSON. A coop action uses initiator + target; self exists for single-entity clip-only one-shots (see Synced actions).
clipstringA full ResourceLocation (e.g. "tiedup:takedown_target") naming the per-role visual clip. See The two clips.
waypointslist (≥ 2)The anchor-local motion path, sampled by the server. Optional in general — a role with no waypoints is clip-only (cosmetic). For a coop action both roles must carry one; it is the presence of waypoints that makes the action position-driving.

Each waypoint:

FieldTypeNotes
tdouble, 0.01.0Normalized time over the session. Strictly increasing across the list — validated at load.
x, y, zdoubleAnchor-local offset in blocks (Minecraft Y-up). Rotated by the anchor yaw and added to the anchor position at runtime.
yawdouble (degrees)Added to the anchor yaw to set the participant’s facing at that waypoint.

The anchor is the initiator’s position and yaw captured when the session starts. Every waypoint (x,y,z) is relative to that anchor (rotated into world space by the anchor yaw), and every yaw is added to the anchor yaw — so an action authored facing “forward” works regardless of which way the initiator was looking.

  • 2 waypoints → straight linear interpolation (constant velocity).
  • 3 or more waypoints → a uniform Catmull-Rom spline through the points (smooth eased curves).

Each waypointed role’s list is validated when the datapack loads. An action is skipped (logged as an error) if a role that declares waypoints has:

  • fewer than 2 waypoints,
  • a t outside [0.0, 1.0],
  • non-strictly-increasing t (catches unsorted lists and duplicate t values),

…or if the def is waypointed but missing a role (a position-driving action needs both initiator and target), or has a decode failure (a wrong type / missing required field).

This is the shipped takedown (data/tiedup/synced_actions/takedown.json) — a 2-second action where the initiator stays put and the target is pulled in from 1.5 blocks ahead, eased to 0.4 blocks (the tackle), facing the initiator (yaw: 180):

{
"duration_ticks": 40,
"proximity_gate": 2.5,
"cinematic_camera": true,
"roles": [
{
"role": "initiator",
"clip": "tiedup:takedown_initiator",
"waypoints": [
{ "t": 0.0, "x": 0, "y": 0, "z": 0, "yaw": 0 },
{ "t": 1.0, "x": 0, "y": 0, "z": 0, "yaw": 0 }
]
},
{
"role": "target",
"clip": "tiedup:takedown_target",
"waypoints": [
{ "t": 0.0, "x": 0, "y": 0, "z": 1.5, "yaw": 180 },
{ "t": 0.5, "x": 0, "y": 0, "z": 0.8, "yaw": 180 },
{ "t": 1.0, "x": 0, "y": 0.2, "z": 0.4, "yaw": 180 }
]
}
]
}

To author your own action, drop a new file at data/<yourns>/synced_actions/<id>.json with the same shape. The id (<id>) becomes the action key; the built-in takedown keybind always requests the takedown id, so a brand-new id is loaded and synced but needs its own trigger to fire (the keybind is hard-wired to takedown).

The two per-role clips 🔧 Manual / tool-gap

Section titled “The two per-role clips ”

Each role’s clip field is a full ResourceLocation that must match a registered animation clip. A complete coop action therefore needs two EF-JSON clips shipped as resourcepack assets — one for the initiator, one for the target — authored exactly like any other pose clip (Blender EF-JSON addon export + the hand-added top-level constructor block that registers it under a namespace:id).

The string in clip must equal the registered id from the clip’s constructor block — not its filename. For the shipped takedown:

Roleclip valueThe clip’s constructor id must be
initiator"tiedup:takedown_initiator"tiedup:takedown_initiator
target"tiedup:takedown_target"tiedup:takedown_target

See Animations for the full clip-authoring workflow (the constructor block, format: "ATTRIBUTES", and the properties unlocks).

Cinematic camera ✅ Available

Section titled “Cinematic camera ”

A coop action can move the camera. There are two modes; the renderer is client-side (🖥️ Client / singleplayer only the camera math runs only on the participating client).

Add an optional top-level camera object with x, y, z doubles. This is an anchor-local offset (same space and rotation convention as the waypoints):

{
"duration_ticks": 60,
"proximity_gate": 2.5,
"camera": { "x": 0.0, "y": 1.5, "z": -3.0 },
"roles": [ /* initiator + target as above */ ]
}

While the action runs, the camera is placed at anchor + rotateYaw(camera, anchorYaw) and aimed at the live midpoint of both participants’ eye positions — so it tracks the action as the bodies move. A terrain clamp pulls the camera in if a block sits between it and the look target, so it never clips through walls. The local participant’s view is forced to third-person for the duration and restored when the session ends.

Behind-player fallback — cinematic_camera

Section titled “Behind-player fallback — cinematic_camera”

If you set cinematic_camera: true but provide no camera block, the action gets a cheap behind-the-player cinematic: third-person view, yaw locked to the authored facing, pitch neutral. The shipped takedown uses exactly this (cinematic_camera: true, no camera).

What you authorCamera behaviour
camera: { x, y, z } presentStatic authored shot, live look-at on the participant midpoint, terrain-clamped.
cinematic_camera: true, no cameraBehind-player third-person, yaw locked to facing, pitch 0.
NeitherDefault view (no camera takeover).

Synced actions & events

The general model — clip-only one-shots, the closed event enum, and action_events.json.

Synced actions & events

Animations

Author the two per-role clips: the required constructor block, format, and pose modifiers.

Animations reference

Bones & regions

The joint names you can pose in those clips, and the body-region vocabulary.

Bones & regions

Item JSON

The per-item camera_offset (distinct from the coop camera) and the bind schema.

Item JSON reference

Planned & partial

Mob-initiated coop (AI auto-initiate) and other not-yet-wired features.

Planned & partial