Godot Native Headless Observer

Supersedes: 20260425-threejs-observer.md

  • Status: deferred (not building yet; focus is on VR (20260425-godot-player.md) first)
  • Deciders: V-Sekai, fire
  • Tags: V-Sekai, Godot, WebTransport, Observer, ZoneServer, Headless, CI, Deferred, 20260425-godot-observer

The Context

The zone server speaks WebTransport (picoquic, UDP 7443 locally / UDP 443 externally). FabricMultiplayerPeer is the C++ peer that wraps picoquic. fabric_client.gd wraps FabricMultiplayerPeer and populates _entity_nodes as CH_INTEREST datagrams arrive. A headless observer that drives this stack proves end-to-end connectivity with the real protocol implementation, with no browser, no TypeScript build, and no service worker.

The Problem Statement

The Three.js observer ((threejsobserver?)) required a separate TypeScript client and runtime. The Godot headless path reuses the identical C++ WebTransport peer and GDScript entity layer that ship with the zone server, closing the “different stack, different bugs” gap and enabling CI smoke tests with a single godot --headless invocation.

Design

CLI

godot --headless --path . \
  --script scripts/headless_log_observer.gd \
  -- [--host=HOST] [--port=PORT] [--dump-json=PATH] [--frames=N]

Defaults: host=127.0.0.1 port=7443 frames=600

Exit 0 when _entity_nodes.size() > 0. Exit 1 on timeout with no entities.

SceneTree script

headless_log_observer.gd extends SceneTree. On _init it instantiates fabric_client.gd, sets zone_host and zone_port, and connects process_frame to a polling handler that reads _entity_nodes from the client every 60 frames. When entities arrive it optionally serialises them to JSON via --dump-json and calls quit(0).

func _collect_entities() -> Array:
    var nodes = client.get("_entity_nodes")
    for k in nodes.keys():
        var node := nodes[k] as Node3D
        if node and is_instance_valid(node):
            out.append({"id": k, "pos": node.global_position})
    return out

Protocol stack

headless_log_observer.gd
    └─ fabric_client.gd
           └─ FabricMultiplayerPeer (C++ / picoquic)
                  └─ WebTransport UDP 7443
                         └─ zone server

No separate datagram parser is needed. FabricMultiplayerPeer handles the 100-byte CH_INTEREST wire format and populates _entity_nodes automatically.

JSON dump (–dump-json)

[{"id": 1, "pos": {"x": 0.0, "y": 0.0, "z": 0.0}}, ...]

Used by shell-based CI orchestration (no browser engine, no Playwright) to assert entities.length > 0 without parsing Godot stdout.

The Downsides

Native-only. The browser operator view (load bars, dot clustering in a <canvas> overlay) is not covered by this ADR. Those features remain in SOMEDAY.

The Road Not Taken

Three.js WebGPU observer ((threejsobserver?)): browser-native, no install, but a separate TypeScript parse layer means bugs in the TypeScript parser can diverge from the C++ implementation. The Godot headless path removes that gap.

Status

Status: Deferred. Not building yet; focus is on the VR client (20260425-godot-player.md) first. The design here remains valid and will be picked back up after the VR pass condition lands.

Decision Makers

  • iFire

Further Reading

(threejsobserver?): 20260425-threejs-observer.md — superseded Three.js Stage 1.

(fabricclient?): modules/multiplayer_fabric/demo/abyssal_vr/scripts/fabric_client.gd — zone client GDScript.

(headlessobserver?): modules/multiplayer_fabric/demo/abyssal_vr/scripts/headless_log_observer.gd — this ADR’s implementation.