# Full-state server demos

The native dedicated server records one `.q4sd` file per map. This is an
OpenQ4 game-state replay, not a capture of UDP packets or WebSocket frames.
The framework calls the multiplayer module's existing server-demo API:

- `WriteNetworkInfo` captures the complete initial game state.
- `ServerWriteServerDemoSnapshot` records PVS-independent world snapshots
  through the reserved `MAX_CLIENTS` demo slot.
- reliable game messages retain their target or exclusion wrapper.
- `ClientReadServerDemoSnapshot`, `ClientProcessReliableMessage`, and
  `ReadNetworkInfo` drive playback in the native OpenQ4 client.

## Lifecycle

`net_serverDemo` defaults to `1`. A recording starts after a multiplayer map
has spawned and closes before map teardown or server shutdown. The writer uses
`<name>.q4sd.part`, flushes an end record, and renames it to `<name>.q4sd` only
after a clean close. Crashed or interrupted writes remain `.part` files and
are never published by the replay API.

Set an explicit output directory with:

```text
+set net_serverDemoPath /srv/q4/replays
```

An empty path writes below `fs_savepath/baseoq4/replays`. Manual controls are:

```text
recordServerDemo
stopServerDemo
playServerDemo /absolute/path/to/match.q4sd
```

`playServerDemo` is a diagnostic client command. It selects the multiplayer game module,
loads the recorded map and initial state, then advances reliable messages and
snapshots at their recorded times. The spectator user command remains active
during playback.

Production viewing uses `playServerDemoServer` in an isolated dedicated process.
The process opens its own UDP port, restores the recorded world as a Q4TV source,
and sends an independent full/delta snapshot chain to every viewer. The playback
clock is frozen until the first viewer reaches `SCS_INGAME` when
`ri_waitForViewer=1` (the gateway default). Later viewers receive the current full
state and continue from that timestamp; they do not download and restart the file.
The gateway explicitly sets `net_serverAllowDataChecksumMismatch=1` for its
non-pure native game and replay processes because browser clients can mount a
different licensed q4base package set. The engine default remains `0`, logs every
accepted mismatch, and never bypasses the pure-server checksum path.

## Container

The little-endian version 1 stream starts with `Q4SD`, the async protocol,
initial frame/time, server info, all user info dictionaries, synchronized
CVars, and the game module's network-info payload. Timed records then contain
either a wrapped reliable game message, a full server-demo snapshot, or the
clean end marker. The writer also creates `<name>.q4sd.meta.json` with map,
mode, start/end time, duration, snapshot count, reliable-message count, and a
native playback command.

## Publication and retention

The Node gateway publishes only finalized `.q4sd` files under
`/api/replays`. `.part`, sidecar, and render-demo files are excluded. Startup,
the listing endpoint, and a one-minute maintenance timer enforce
`Q4_REPLAY_RETENTION`, which defaults to the newest 300 recordings and removes
matching metadata sidecars with expired recordings.

`POST /api/replays/<id>/watch` starts or reuses a process from `Q4_REPLAY_PORTS`
(default `28040,28041,28042,28043,28044,28045,28046,28047`) and returns
`port`, `connectUrl`, `replay`, `replayId`, `map`, and `reused`. Active processes
are reported as `replayInstances` by `/api/status`. The gateway admits
`/game/<port>` only while the corresponding child process owns that port. Finished
replays have a 15-second final-score grace; idle instances expire after 10 minutes
and every instance has a one-hour hard cap by default.

Playback processes explicitly set `sys_allowMultipleInstances=1`. The dedicated
binary already bypasses the POSIX client-instance lock through
`net_serverDedicated=1`, but the explicit setting preserves the process-pool
contract if the playback executable changes. Native multi-viewer smoke clients
also opt in because two graphical viewers must coexist during the gate.

## Verification

Build from the pinned sources and tracked patches, then run the dedicated
smoke test:

```bash
tools/build-native.sh --clean --jobs 4
port/native/smoke-dedicated.sh
port/native/smoke-server-demo-playback.sh
port/native/smoke-server-demo-join.sh
```

`Q4_SMOKE_RECORD_SECONDS` controls how long the first-map recording runs after
all eight smoke bots join (default `70`; positive integer seconds). The default
stays above the late-join gate's 60-second minimum so a release never needs a
recording from an older, declaration-incompatible build.

The smoke test starts a real `mp/q4web_arena` match with eight bots, reloads that
bot-ready map, and then shuts down through the engine console. It requires one
complete demo on each side of the map rotation and rejects partial, malformed,
empty, or incomplete output. The bundled arena is used because stock Quake 4
multiplayer maps do not ship bot AAS data. The playback smoke runs the staged
native client under Xvfb and requires the recorded stream to reach its clean
end. When no demo path is supplied, playback selects the largest complete smoke
recording (mtime breaks ties), avoiding the short trailing recording that can be
created immediately before a rotation test shuts down. Late-join uses the same
ordering inside `runtime/native-smoke/replays`; it consults the legacy production
`replays` directory only when the smoke directory contains no valid `Q4SD` file
with complete metadata.
The join smoke starts a dedicated playback port, verifies that time remains frozen
before a viewer, connects one native client, then connects a second client after
playback has advanced and requires both clients to receive live repeater snapshots.
