Skip to content

clips

Record gameplay clips and query them frame by frame.

The clips tool is the backbone of the dashcam workflow. It writes every physics frame of spatial data to disk, so you can scrub through a timeline of exactly what happened — positions, velocities, and properties — at any frame in the recording.

When to use it

  • Capturing a bug: record while playing, mark the bug moment, analyze the clip
  • Post-mortem analysis: something went wrong in a playtest — query what happened
  • Regression testing: record expected behavior, compare against future runs
  • Long sessions: in-memory ring buffer holds ~10 seconds; recordings hold hours

Parameters

ParameterTypeRequiredDescription
actionany
"add_marker" | "save" | "status" | "list" | "delete" | "markers" | "snapshot_at" | "trajectory" | "query_range" | "diff_frames" | "find_event" | "screenshot_at" | "screenshots"
requiredAction to perform.
at_framenumber optional Frame number for snapshot_at.
at_time_msnumber optional Timestamp (ms) for snapshot_at. Finds nearest frame.
clip_idstring optional Clip to operate on (from list response). Defaults to most recent clip if omitted.
conditionany optional Condition for query_range. Object with "type" key. Types: "moved" (threshold), "proximity" (target, threshold), "velocity_spike" (threshold), "property_change" (property), "state_transition" (property), "signal_emitted" (signal), "entered_area", "collision". Example: {"type": "proximity", "target": "walls/*", "threshold": 0.5}
detailstring optional Detail level for snapshot_at: "summary", "standard", "full".
event_filterstring optional Event filter for find_event (substring match).
event_typestring optional Event type for find_event.
frame_anumber optional Frame A for diff_frames.
frame_bnumber optional Frame B for diff_frames.
from_framenumber optional Start of frame range for query_range / find_event.
marker_framenumber optional Frame to attach marker to (add_marker). Defaults to current.
marker_labelstring optional Marker label (add_marker, save).
nodestring optional Node path for query_range.
propertiesstring[] optional Properties to sample in trajectory. Default: ["position"]. Options: position, rotation_deg, velocity, speed, or any state property name.
sample_intervalnumber optional Sample every Nth frame for trajectory. Default: 1.
to_framenumber optional End of frame range for query_range / find_event.
token_budgetnumber optional Soft token budget.

Actions

start

Begin recording. A new clip is created and data is written to disk on every physics tick.

json
{
  "action": "start",
  "clip_id": "chase_bug_01"
}

If clip_id is omitted, a unique ID is generated automatically (e.g., clip_1741987200).

Response:

json
{
  "action": "start",
  "clip_id": "chase_bug_01",
  "result": "ok",
  "record_path": "/tmp/theatre-clips/chase_bug_01.clip"
}

stop

Stop the current recording.

json
{
  "action": "stop"
}

Response:

json
{
  "action": "stop",
  "clip_id": "chase_bug_01",
  "result": "ok",
  "frame_count": 512,
  "duration_ms": 8533,
  "file_size_bytes": 204800
}

mark

Mark the current frame as a point of interest (e.g., "bug happened here"). This is what the F9 key triggers from the editor dock.

json
{
  "action": "mark",
  "label": "player_clips_wall"
}

Response:

json
{
  "action": "mark",
  "clip_id": "chase_bug_01",
  "frame": 337,
  "label": "player_clips_wall",
  "result": "ok"
}

list

List all available clips.

json
{
  "action": "list"
}

Response:

json
{
  "clips": [
    {
      "clip_id": "chase_bug_01",
      "frame_count": 512,
      "duration_ms": 8533,
      "created_at": "2026-03-12T14:30:00Z",
      "markers": [
        { "frame": 337, "label": "player_clips_wall" }
      ]
    }
  ]
}

query_frame

Get the complete spatial state at a specific frame.

json
{
  "action": "query_frame",
  "clip_id": "chase_bug_01",
  "frame": 337,
  "nodes": ["Player", "Wall_East"],
  "detail": "full"
}
ParameterTypeDescription
clip_idstringWhich clip to query
frameintegerFrame number (0-based)
nodesstring[]Limit to these nodes (optional)
detailstring"summary" or "full"

Response:

json
{
  "clip_id": "chase_bug_01",
  "frame": 337,
  "timestamp_ms": 5617,
  "nodes": {
    "Player": {
      "class": "CharacterBody3D",
      "global_position": [8.92, 0.0, -3.14],
      "velocity": [45.3, 0.0, 0.0]
    },
    "Wall_East": {
      "class": "StaticBody3D",
      "global_position": [9.0, 0.0, -3.0]
    }
  }
}

query_range

Query multiple consecutive frames at once. The primary tool for analyzing a bug across time.

json
{
  "action": "query_range",
  "clip_id": "chase_bug_01",
  "start_frame": 325,
  "end_frame": 350,
  "nodes": ["Player"],
  "detail": "summary",
  "stride": 1
}
ParameterTypeDefaultDescription
clip_idstringrequiredWhich clip to query
start_frameintegerrequiredFirst frame (inclusive)
end_frameintegerrequiredLast frame (inclusive)
nodesstring[]allNodes to include
detailstring"summary"Data level per node
strideinteger1Sample every N frames
conditionobjectnullFilter frames by condition

Condition filtering:

Use condition to include only frames where something specific is true:

json
{
  "condition": {
    "type": "proximity",
    "nodes": ["Player", "Wall_East"],
    "max_distance": 1.0
  }
}

Other condition types:

  • { "type": "velocity_above", "node": "Player", "threshold": 20.0 } — frames where the node's speed exceeds threshold
  • { "type": "property_equals", "node": "Player", "property": "on_floor", "value": false } — frames where a property matches a value

delete

Delete a clip file from disk.

json
{
  "action": "delete",
  "clip_id": "chase_bug_01"
}

Response:

json
{
  "action": "delete",
  "clip_id": "chase_bug_01",
  "result": "ok"
}

Example conversation

I recorded the tunneling bug. The marker was at frame 337. What was happening?
Found clip chase_bug_01 with 512 frames and 1 marker at frame 337 labeled player_clips_wall. Querying the frames around the marker.
Clear velocity spike at frame 337: Player velocity jumps from 14.2 m/s at frame 330 to 45.3 m/s at frame 337 — a 3x increase in one frame (16ms). At that velocity, the CharacterBody3D travels 0.75 meters per tick, which is wider than the Wall_East CollisionShape3D. The move_and_slide continuous collision detection did not catch it because the shape is thinner than the per-tick displacement. Fix: enable CCD in the CharacterBody3D or thicken the wall collision shape to at least 1.0 unit.

Tips

Always specify nodes in query_range. Even small clips with all nodes and detail: full can be enormous. Filter to the 2-3 nodes relevant to the bug.

Use stride: 5 for long recordings. Instead of every frame, sample every 5th frame for a quick scan. Then use query_frame to drill into specific moments.

Use conditions to filter. The proximity condition is especially powerful — it finds frames where two nodes are closer than a threshold, which is exactly when collision bugs occur.

Markers are set automatically with F9. In the editor dock, pressing F9 calls clips { "action": "mark" } with a default label. You can also add more labels by calling the tool directly during a session.

Open source under the MIT License.