Introspection

The Runtime Introspection Layer (engine.introspect) provides structured, JSON-serialisable views of live engine state without globals or allocations. It is the single source of truth consumed by the Remote Debug Protocol, the MCP server, and the Studio inspector.


Key types

const engine = @import("engine");

// A read-only view of one loaded scene handed to the inspector by the host.
const SceneView = engine.introspect.SceneView;

// Everything the inspector can see at one instant.
const World = engine.introspect.World;

// A typed value for mutation operations.
const Value = engine.introspect.Value;

Providing a World snapshot

The host (game or studio) implements a callback that returns a World:

fn myWorldProvider(ctx: ?*anyopaque) engine.introspect.World {
    const state: *GameState = @ptrCast(@alignCast(ctx));
    return .{
        .scenes = state.sceneViews(),
        .metrics = &state.metrics,
    };
}

The callback is called on the debug server's background thread once per request. It must return a read-only view safe for concurrent access — a frame snapshot is sufficient.


Serialisation helpers

// Snapshot the entire world as JSON (heap-allocating).
const json = try engine.introspect.snapshotJsonAlloc(allocator, world);
defer allocator.free(json);

// Or stream directly into any std.json.Stringify:
try engine.introspect.writeSnapshot(&jw, world);

// Schema of all built-in component types:
try engine.introspect.writeSchema(&jw);

Queries (allocation-free)

var indices: [128]usize = undefined;

// Find by name substring:
const count = engine.introspect.findByName(scene.nodes, "Player", &indices);

// Find by component type:
const count = engine.introspect.findByComponent(scene.nodes, "Camera", &indices);

// Check component presence:
const has = engine.introspect.hasComponent(node, "Light");
const ci  = engine.introspect.componentIndex(node, "Light"); // ?usize

Metrics

engine.Metrics is the single write-target for all engine subsystems:

engine.Metrics.recordFrame(dt_seconds);   // call once per frame
engine.Metrics.beginFrame();              // resets per-frame render counters
engine.Metrics.draw_calls += 1;           // filled in by the renderer

The debug server and MCP server read world.metrics to answer metrics and get_metrics calls.


Mutation

// Set a field on a built-in component:
try engine.introspect.setComponentField(node, "Camera", "fov", .{ .number = 90 });

// Set a transform field:
try engine.introspect.setTransformField(node, "position.x", .{ .number = 10 });

// Spawn / destroy (queued, applied next frame):
try engine.introspect.spawnEntity(scene, "NewEnemy");
engine.introspect.destroyEntity(scene, node_index);

Over the protocol, mutations are reachable when the debug server runs in read-write mode (allow_write / CLI --rw). The host registers a MutationApplier that the server's main-thread pump invokes, so edits apply to live state safely. The Studio wires this through its undo stack — see the Remote Debug Protocol page for the full method and event list.


← All docs Edit this page