# q4 - Quake 4 in the browser

Unofficial, non-commercial OpenQ4 browser port. The browser runs the original
single-player or multiplayer GameLib against an operator-authorized Quake 4 data
catalog cached in browser-private storage; a native OpenQ4 dedicated process is the
authoritative public multiplayer server. Production is `https://q4.suby.pl` on
gateway port 3280 and game UDP port 28004.

Read `/srv/GAME.md` before changing cross-game contracts. Deployment and recovery are
in `DEPLOY.md`; area notes live in `port/` and the data-mod pipeline in
`mods/README.md`.

## Architecture

```text
browser
  | HTTPS: shell, immutable WASM, CC0 runtime, gated retail catalog, replay API
  | WSS /game/<port>: one binary WS frame == one UDP datagram
  v
server.js :3280 (Node ESM, dependency: ws)
  |- public/                 UI, PWA, touch/gamepad, identity, voice HUD
  |- engine/                 frozen SP and MP OpenQ4 WebAssembly builds
  |- content/assets/baseoq4/ minimal CC0 pak0/pak1 + approved CC0 map PK4s
  |- /retail/                exact SHA-addressed, deployment-gated PK4 allowlist
  |- server/udp-bridge.js    rate-limited WS <-> loopback UDP bridge
  |- native OpenQ4 ded       authoritative DM, bots, full-state .q4sd recording
  `- replay playback pool    isolated native processes/UDP ports, join-anytime

browser OPFS
  /q4-retail/revisions/<revision>/runtime/q4base  proprietary player PK4s
  /q4-runtime/<build>/baseoq4                    shared self-authored CC0 data
  /q4-user                                       saves/config
```

The production operator has confirmed authorization for this deployment to deliver
the required retail data. The gateway exposes it only when
`Q4_RETAIL_DELIVERY=operator-authorized`, and only as the exact catalog `pak001.pk4`
through `pak022.pk4` plus
`zpak_english.pk4`. The manifest uses content-hashed URLs; the browser verifies every
SHA-256 before activating a revision in OPFS. There is no generic retail directory
mount. `assets-src/` and `runtime/` remain ignored operator state, outside Git, the
public tree, generated content, and the source offer. The native server uses the same
operator data via `runtime/native/q4base -> assets-src/gog/q4base`. Do not move that
symlink or its target below `public/`, `content/`, or `engine/`.

Public `pak0.pk4` and `pak1.pk4` are deterministic, self-authored CC0 integrity
packs; they contain configuration/license text and two small declaration-compatibility
files, not upstream or retail media. The
approved q4web arena addon is also self-authored CC0. Staging audits every public PK4
entry against the private retail corpus by path and content hash; the allowlist is
empty by default.

## Engine variants

- `singleplayer`: the GPL OpenQ4 Emscripten `MAIN_MODULE` loads the separately built
  OpenQ4-GameLibs `src/game` `SIDE_MODULE` through `GetGameAPI`.
- `multiplayer`: the GPL OpenQ4 Emscripten `MAIN_MODULE` loads the separately built
  `src/mpgame` `SIDE_MODULE`; it must retain the multiplayer wire protocol
  (`ASYNC_WRITE_TAGS=1` and EVENT_ACK) and the local bot port.
- `replay`: normally the MP artifact connecting as a spectator to a playback port.

The variants and their GameLib side modules are deliberately separate in
`engine/engine-manifest.json`. The loader verifies each side module's SHA-256 and
stages it in WasmFS before the engine calls `dlopen`. Never point both entries at an
SP build just because it links, and never fold a GameLib back into the engine module.
The native server is the protocol oracle; validate a real WASM connection after any
GameLib/network change.

OpenQ4 engine code is pinned to `975e296b80b66f63b817d16ea78b53e0685c5c55`
and GPLv3.
OpenQ4-GameLibs is pinned to `eb26e5f3a52358c58ee00bee5602575e5b057164` and remains
under the Quake 4 SDK Limited Use EULA, not GPL. Full local notices are under
`public/legal/`; `tools/stage-source-offer.sh` publishes the exact modified source as
a code-only snapshot which omits upstream media without established redistribution
rights.
The WebAssembly module boundary mirrors OpenQ4's native dynamic GameLib boundary; it
is an implementation fact, not a determination that resolves licensing questions.

## Web platform contracts

- WebGL 2 / GLSL ES 3.00 only. The renderer implementation is under the pinned
  OpenQ4 tree at `src/renderer/webgl2`; keep shader link failures fatal and tested.
- Emscripten mounts OPFS with WasmFS at `/opfs`. `fs_basepath` points to the active
  `/q4-retail/revisions/<revision>/runtime` tree containing retail `q4base`;
  normally this is downloaded automatically from the verified server manifest, while
  manual local import remains an optional fallback. `fs_cdpath` points to
  `/q4-runtime/<build>` containing only the minimal CC0 `baseoq4` packs/addon;
  `fs_savepath` points to user data.
- `Module.q4WebSocketUrl` is same-origin `ws(s)://<host>/game/<port>`. Never hardcode
  `suby.pl`, because the same build must work through `*.z6.mm.mk:11443`.
- JSON text frames on the game socket are reserved for WebRTC voice signaling. Binary
  frames are opaque idTech datagrams. PTT is `T`; voice is created only for a human
  multiplayer client, never for bots or replay spectators.
- `public/js/voicechat.js` is an absolute symlink to the canonical
  `/srv/_shared/voice/voicechat.js`. Do not replace it with a copied file or edit it
  here. `voicehud.js` is intentionally local.
- The cookie/beacon/direct-route helpers in `public/js/identity.js` must remain aligned
  with `/srv/gry/suby-identity.reference.js` for both domain families.
- `/healthz` must keep a numeric live-client field and
  `Cross-Origin-Resource-Policy: cross-origin`; safe restart and direct-route probing
  depend on both.

## Content and mods

`tools/import-retail.sh` extracts the operator's GOG installer into the ignored retail
directory. `tools/stage-retail-manifest.mjs` hashes the exact 23-file English catalog
and writes the ignored deployment manifest. `public/js/importer.js` automatically
downloads those hash-addressed files, verifies each SHA-256, and activates the complete
revision in OPFS. Interrupted downloads resume with HTTP Range requests. The optional
manual import path still requires pak001-pak022 plus one language zpak and never
uploads selected local files.

`node tools/ingest.mjs <map.pk4> --license ... --source ... --author ...` validates a
data-only mod, compiles missing `.proc`, `.cm`, and `.aas32`, rejects executable code
and archive attacks, emits `mods/dist/q4web_<slug>-<hash>.pk4`, and restages content.
Every multiplayer map must include valid `DewmAAS 1.08` navigation. The CC0 test map
source is `mods/maps/q4web_arena/`.

## Replays

Every native match records a finalized full-state `.q4sd`; `.part` files are never
listed. The API retains the newest 300. Watching a replay must allocate/reuse a
dedicated playback instance and return a spectator port. A second viewer joining after
playback has started must receive current state through ordinary snapshots. Downloading
the file and replaying from timestamp zero is useful for diagnostics but does not meet
the join-anytime contract.

## Build and test

```bash
tools/sync-upstream.sh
tools/import-retail.sh                       # once; proprietary output is ignored
node tools/stage-retail-manifest.mjs          # ignored, exact 23-file server catalog
tools/build-native.sh --clean --jobs 4
tools/build-wasm.sh --clean --jobs 4          # canonical SP/MP build/stage entrypoint
node tools/stage-web-content.mjs
npm test
npm run test:zip-cache
node vendor/openq4/tools/tests/webgl2_shader_smoke.mjs
python3 tools/test-wasm-network.py
Q4_E2E_URL=http://127.0.0.1:3280 npm run test:e2e:shell
```

Before release, also run the native bot/demo/playback smokes in `port/native/`, the
WebGL shader smoke, the WASM network test, and the full Playwright suite against the
enabled local retail catalog. Inspect desktop and mobile screenshots, not only exit
codes.

## Operational traps

1. Restart only with `tools/safe_restart.sh`; exit 2 means leave the live service alone.
2. Caddy's admin-API route is ephemeral. `/etc/cron.d/q4-caddy-watchdog` must remain.
3. Never add vendor Git databases, retail files, native runtime, replays, or generated
   content to Git. Frozen files in `engine/` are intentionally tracked.
4. OpenQ4 startup can look healthy before a map loads. Release evidence requires an
   actual rendered retail map and an actual MP connection, not just `main()` or shader
   compilation.
5. Keep SP and MP source staging isolated. PCH reuse across them can silently compile
   the SP protocol into a file named as MP.
6. A map that renders but has no valid `.aas32` is not bot-ready.
7. Public shell changes are live without restart; Node/native/engine changes require a
   drained restart. Restage the source offer and content manifests before release.
