<p align="center">
  <img src="https://www.cyberiaonline.com/apple-touch-icon-precomposed.png" alt="CYBERIA online"/>
</p>

<div align="center">

<h1>cyberia client</h1>

</div>

# Cyberia game client

**Path:** `cyberia-client/` · **Language:** C11/GNU11 → WebAssembly (Emscripten) · **Role:** presentation runtime for Cyberia

`cyberia-client` is the rendering and interactive runtime for the Cyberia MMO extension on Underpost Platform. It captures input, predicts the local player, reconciles against authoritative snapshots, interpolates remote entities, and renders the world. It owns the render policy locally.

It is **not** a world-simulation authority. The authoritative simulation runs on `cyberia-server`. Persistent content and asset metadata come from `engine-cyberia` via REST.

---

## Operating model

Three independent processes, non-overlapping roles. The ecosystem is playable only when all three are running and healthy at the same time.

```text
engine-cyberia          cyberia-server          cyberia-client
content authority       simulation authority    presentation runtime
  ^ REST                  | WebSocket binary
  | atlases, asset        | snapshots  (server -> client)
  | metadata, optional    | input cmds (client -> server)
  | client hints          v
  `---------------- Browser tab: prediction, reconciliation, interpolation, render
```

- Each service owns its own monitor and reconnector.
- The client reconnects to `cyberia-server` over WebSocket and fetches content from `engine-cyberia` over REST.
- If any of the three services is unhealthy, the game moves to standby until all three recover.

The client speaks two transports:

- **WebSocket binary** to `cyberia-server` for snapshots and input commands
- **REST** to `engine-cyberia` for atlas frames, asset blobs, and optional client-hints overrides

Asset distribution flows through Underpost Platform's static and PWA pipeline.

---

## Render loop

The render loop runs at vsync. Tick simulation is decoupled from render frames via a fixed-timestep accumulator. Step ordering inside one render frame:

1. **Optional client hints poll** — advance the asynchronous hints fetch state machine.
2. **Input capture** — drain raw OS events.
3. **Input dispatch** — UI hit-test → build typed input command (`kind`, `clientTick`, `sequence`, payload) → apply to prediction → send on the wire.
4. **Reconciliation** — apply latest snapshot's `lastAcked`: drop acknowledged input commands from the replay buffer; rewind predicted self to authoritative position; replay unacked input commands.
5. **Fixed-timestep prediction** — while accumulator ≥ `tickDuration`, advance prediction one tick.
6. **Interpolation** — compute remote-entity view positions at `renderTick = serverTickEstimate − INTERP_TICKS`.
7. **Render** — read view models; never mutate world state.

Render frame rate is independent of simulation tick rate. The fixed-timestep accumulator ensures the predicted simulation advances at the authoritative `tickRate` regardless of FPS.

---

## Module map

Directional dependency: from outermost (UI / render) inward toward `domain/`. Nothing in `domain/` imports outward.

```
src/
  domain/
    tick.h                          tick rate constants, monotonic types
    presentation_runtime.{c,h}      sole owner of presentation: async fetch of
                                    /api/v1/cyberia-client-hints/:CYBERIA_CLIENT_HINTS_CODE,
                                    palette/status-icon/camera accessors, tiny
                                    inline bootstrap fallback
  input/
    input_command.{c,h}             typed InputCommand factory; sequence allocation
  prediction/
    prediction.{c,h}                predicted self position; replay buffer; reconcile
  interpolation/
    interpolation.{c,h}             remote entity view positions at renderTick
  network/
    session.{c,h}                   last server tick, last acked sequence, tick estimate
    client.{c,h}                    WebSocket lifecycle + binary I/O
    socket.{c,h}                    Emscripten WebSocket bridge
  ui/                               modals, HUD, inventory bar, FCT, nameplates
  game_state.{c,h}                  world state mirror (gameplay subset only)
  binary_aoi_decoder.{c,h}          server snapshot parser
  game_render.{c,h}, render.{c,h}   render pipeline
  main.c                            entry point + render loop
  js/services.{js,c,h}              REST fetch bridge (atlas, ui-icons, client-hints)
```

| Owner                         | Owns                                                                                                                                                              | Reads                                             | Writes                                                                                          |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `domain/presentation_runtime` | the entire presentation surface (palette, entity colour keys, status icons, camera, cell, interpolation). Inline bootstrap fallback while the fetch is in flight. | JSON response from `/api/v1/cyberia-client-hints` | own table + one-shot hydration of `g_game_state.cell_size`, `.interpolation_ms`, `.camera.zoom` |
| `input/input_command`         | typed InputCommand factory                                                                                                                                        | session for tick + sequence                       | —                                                                                               |
| `prediction/`                 | predicted self position, input replay buffer                                                                                                                      | snapshot self + session ack                       | predicted self                                                                                  |
| `interpolation/`              | remote entity `interp_pos`                                                                                                                                        | snapshot history                                  | remote `interp_pos` only                                                                        |
| `network/session`             | last server tick, last acked sequence                                                                                                                             | snapshot header                                   | session singletons                                                                              |
| `binary_aoi_decoder`          | wire parser                                                                                                                                                       | binary frames                                     | `game_state` + session + prediction (via callback)                                              |
| `game_state`                  | gameplay world mirror                                                                                                                                             | —                                                 | —                                                                                               |
| `render/`, `ui/`              | rendering                                                                                                                                                         | view models                                       | screen                                                                                          |

`game_state` holds only the gameplay subset of world state (entities, positions, life, AOI, equipment, frozen flag, coins). It does not carry palette state, status-icon visuals, or any other presentation field.

---

## Presentation ownership

The client owns its render policy. The authoritative server holds no presentation state.

Three layers, strictly inward-dependent:

1. **`domain/presentation_runtime.{c,h}`** — sole owner of every presentation value. Fires a single asynchronous GET against `/api/v1/cyberia-client-hints/:CYBERIA_CLIENT_HINTS_CODE` at startup, polled once per render frame. When the response settles, parses palette, entity colour keys, status-icon visuals, and camera/cell tunings into a process-local table and writes a one-shot hydration into `g_game_state` (cell_size, interpolation_ms, camera.zoom). The C client carries NO compile-time palette or status table — only a tiny inline neutral-grey bootstrap so the splash screen has something to draw while the fetch is in flight. The canonical schema for the response lives at engine-cyberia's `src/client/components/cyberia/SharedDefaultsCyberia.js`.
2. **Renderers** — call `presentation_runtime_palette("KEY")`, `presentation_runtime_status_icon(u8)`, `presentation_runtime_status_border(u8)`, `presentation_runtime_entity_fallback_color(entity_type)` at each use site.

What lives in this layer:

- Named palette (colors).
- Per-entity-type fallback color keys.
- Status-icon visual table (icon stem + border color per numeric ID).
- Camera smoothing / zoom defaults.
- Interpolation window.
- Dev-overlay flag.
- Viewport screen factors.

What does **not** travel on the WebSocket init or AOI streams:

- Palette.
- Camera knobs.
- Dev-overlay flag.
- Interpolation window.
- Status-icon visuals.
- Per-entity-type color keys.

The numeric status-icon u8 still rides on the AOI wire — that is the protocol-level half. The icon stem and border color are presentation and are resolved entirely on the client.

### Optional client hints

`GET /api/v1/cyberia-client-hints/:instanceCode` (engine-cyberia REST, **not** cyberia-server) returns a JSON document mirroring the structure of the compile-time defaults. The client is required to function with no successful call to this endpoint:

- 200 with `{ palette, entityColorKeys, statusIcons, cameraSmoothing, cameraZoom, defaultWidthScreenFactor, defaultHeightScreenFactor, interpolationMs, devUi }` — overrides applied on top of defaults.
- 404 if the instance has no overrides — defaults are used.
- Network error — defaults are used.

No authentication; presentation hints are not secret. The Go authoritative server never calls this endpoint.

---

## Tick-aware components

### Session

`network/session.{c,h}` tracks per-connection tick state:

- `last_server_tick` — highest snapshot tick observed.
- `last_acked_input_sequence` — highest sequence the server has applied for this client; echoed in every snapshot header.
- `next_input_sequence` — monotonic counter for outgoing input commands.
- `server_tick_estimate` — extrapolation of last server tick by elapsed wall time, used to stamp outgoing input commands and to compute `renderTick`.

### Prediction

`prediction/prediction.{c,h}` owns the predicted self position. It is the only writer of the predicted state. Internally it tracks **two** positions: the discrete per-tick `predicted_pos` (where the simulation says the player is) and the continuous render-frame `display_pos` (what the renderer reads). The renderer never sees the discrete value.

- `prediction_apply(cmd)` — optimistic local apply of an input command + push onto replay buffer.
- `prediction_step(dt)` — fixed-timestep advance, called by the accumulator. Mutates `predicted_pos` only.
- `prediction_reconcile()` — on snapshot arrival: drop acknowledged inputs, rewind self to authoritative position, replay unacked inputs. Mutates `predicted_pos` only.
- `prediction_display_step(frame_dt)` — per-render-frame exponential lerp of `display_pos` toward `predicted_pos`. This is the layer that absorbs reconcile snaps and sim-tick stepping so the visible main player moves continuously.
- `prediction_self_position()` — render-time accessor; returns the smoothed `display_pos`.

### Interpolation

`interpolation/interpolation.{c,h}` owns remote-entity render-time positions. It is the only writer of remote `interp_pos`. It never touches the local player (prediction owns that). Sampling happens at `renderTick = serverTickEstimate − INTERP_TICKS`.

### Input command pipeline

```
mouse / touch / keyboard
        │
        ▼
input/input_capture
        │
        ▼
UI hit-test cascade  ──► modal/inventory/HUD consumed?
        │  no
        ▼
input_command_build_*   stamps client_tick + sequence
        │
        ▼
prediction_apply        optimistic local apply
        │
        ▼
uplink send             WebSocket binary frame
```

`InputCommand.Sequence` is allocated by `session_next_input_sequence`. Monotonic, never reused. The server echoes the highest applied sequence in every snapshot header; the prediction module drops acked commands from its replay buffer on reconciliation.

---

## Binary AOI snapshot format

The client decodes server-pushed AOI frames. Header (little-endian, 11 bytes for `0x01`/`0x03`):

```
[0]      u8   msgType     0x01 aoi_update | 0x03 full_aoi
[1..4]   u32  tick        simulation tick at which the snapshot was produced
[5..8]   u32  lastAcked   highest InputCommand.Sequence applied for this client
[9..10]  u16  entityCount entity blocks that follow
```

After parsing the header, the decoder invokes `session_on_snapshot(tick, lastAcked)` and then `prediction_reconcile()` so the predicted self stays consistent with the just-arrived authoritative state.

Other message types — init data (0x02), FCT (0x04), ItemFCT (0x05) — carry their own headers and are not part of the per-tick replication stream.

---

## REST fetches (engine-cyberia)

The client speaks REST directly to engine-cyberia for content. None of these calls go through cyberia-server.

The engine resolves an item label to the definition the catalog binds, then to that definition's
atlas. The atlas blob is the primary render: the bytes the canonical render CID addresses, at one
pixel per cell. The atlas metadata describes that same render. The engine also keeps an upscaled
derived render for viewing; the client never downloads it. The idle preview is the first down-idle
frame scaled to fill 300 px, with no margin. The interact overlay and every engine editor show it as an item's
picture, and the editors share one loaded copy per item. `cyberia ol --sync` derives both again from the primary render of stored items, then draws again the preview of each map that shows them and reloads the object layers of the game servers ([`ol --sync`](../reference/cyberia-cli.md#cyberia-ol--object-layer)).
The [render contract](../../object-layer/explanation/render-contract.md) defines these terms.

| Endpoint                                                          | Purpose                                     |
| ----------------------------------------------------------------- | ------------------------------------------- |
| `GET /api/v1/atlas-sprite-sheet/metadata/:itemKey`                | Frame layout JSON for a sprite atlas        |
| `GET /api/v1/atlas-sprite-sheet/blob/:itemKey`                    | Primary render PNG, one pixel per cell      |
| `GET /api/v1/atlas-sprite-sheet/idle-preview/:itemKey`            | 300 px idle preview PNG, the item's picture |
| `GET /api/v1/object-layer/:itemId`                                | ObjectLayer JSON metadata                   |
| `GET /api/v1/cyberia-dialogue/code/default-:itemId`               | Dialogue lines for an NPC                   |
| `GET /assets/ui-icons/:iconId.png`                                | Status-bar icons                            |
| `GET /api/v1/cyberia-client-hints/:instanceCode`                  | Optional presentation overrides             |
| `GET /api/v1/cyberia-instance/instance-map/:instanceCode/static`  | Instance Map graph (nodes, edges, POIs)     |
| `GET /api/v1/cyberia-instance/instance-map/:instanceCode/dynamic` | Instance Map provider activity (~1/s poll)  |

All requests are CORS-simple GETs (no preflight) and cacheable. None require credentials.

---

## Instance Map

The Instance Map is a strategic overlay, not a minimap. It visualises the instance as a packed, gapless grid of map cards with animated portal edges, for navigation and planning. It never reproduces the gameplay viewport.

One integrated widget with two modes, split across two modules:

- **Toolbar** (`ui/toolbar`) — the top HUD strip: the compact map readout left-aligned, and the right-hand toggle row `[quest][map][fullscreen]`. The quest button shows/hides the Quest Journal and the map button shows/hides the minimap; both are hidden by default and stay open together. The coordinate readout opens the expanded Instance Map, which morphs to the full screen with an eased transition and retracts the same way on close.
- **Side stack** (`ui/hud_side_stack`) — the right-hand column both panels live in: the minimap on top, the Quest Journal beneath, between the toolbar and the inventory bar. The column sizes to the room the bar leaves. When both are open the minimap gives way to at most half of the column so the journal keeps room, and the journal takes only what its content needs. Each panel keeps its own state across toggles: the journal its scroll, sections and pages, the minimap its zoom.
- **Container** (`ui/modal_map`) — compact mode is the always-on readout (map code, position, fps) hosted inside the toolbar; expanded mode is the full-screen morph target.
- **Content** (`ui/modal_instance_map`) — renders the Instance Map inside the container: a non-blocking translucent full-screen panel (the world keeps rendering behind it).

Strict independence from the gameplay renderer:

| Concern    | Owner                                                                                  |
| ---------- | -------------------------------------------------------------------------------------- |
| Graph data | `ui/instance_map_data` — engine-cyberia REST only, never the AOI stream                |
| Camera     | own pan/zoom with smooth exponential interpolation (drag pan, wheel zoom, pinch zoom)  |
| Layout     | deterministic row-major map-card grid with quarter-turn rotation, computed client-side |
| Selection  | tap a node → contextual panel (name, quest/action provider counts, portal links)       |

Data lifecycle:

1. Opening the overlay fetches the **static** payload once: graph nodes, portal edges, and authored `presencePois` carrying map-cell presence status and static action/quest capability membership. The instance code arrives in the simulation server's `metadata` message.
2. While open, the client polls the **dynamic** endpoint (~1/s, `?playerId=`) only for per-player capability activity: `acceptable` / `active` quests and active actions.
3. Closing the overlay stops polling immediately; late responses are discarded.

Live player position never travels through this API — engine-cyberia holds no simulation state. The overlay marks the player's node (and cell fraction within it) from the client's own predicted position, refreshed every frame.

At normal zoom, POIs render only their authored presence-status icon. At inspection zoom, static action and quest capability icons appear; dynamic activity only changes capability emphasis. The local player's live presence and authoritative stats sum remain client-side. Decorative map content without a presence status is not rendered as a POI.

---

## Stats and progression

Every living entity's overhead HUD shows `[stats] lv.<level>` and `[stack] <stats sum>` above its nameplate, and a gold XP bar under its HP bar. The local player's bar fills with its progress through the current level and carries an `XP n / m` label in a smaller font; entities whose XP the client cannot read show an empty, unlabeled bar.

The Stats tab of the interact modal draws one shared stat panel per block: the stats icon, the signed sum, and a two-column grid with one icon per stat. Every entity gets its effective stats. The local player also gets the OL modifiers, base stats, and temporary modifiers from the self snapshot. The inventory modal draws the same panel for one item.

An XP award floats as gold `+N XP` combat text over the earner only. A server level increment triggers a `LEVEL UP` aura: shaded gold sparks stream from the entity's feet up past its head for 3 s under a spinning star and a label, and the whole effect follows the entity as it moves, for any entity in view. First sight, repeated snapshots, and lower levels do not trigger it.

A defeat of a player or bot triggers the death cue: shaded red sparks burst out of the body and fall while a `skull` icon rises over it for 2.6 s, following the body. It fires once, when an entity seen alive in the prior snapshot is dead in this one. An entity first seen dead, or still dead, triggers nothing. A resource is extracted, not slain: it goes straight to its extracted state with no cue.

While either cue runs, the entity's whole overhead stack, presence icon included, yields the space to it and returns when the cue ends.

The cues are visual only. Their sounds arrive as `audio_event` messages the server broadcasts to every player whose area of interest covers the event, so a defeat, an extraction, or a level gained sounds the same for everyone in reach. The client plays the id through the map's audio bindings.

Item slots badge an object layer with the `stack` icon and its signed stat sum, red when negative.

The server sends six values in canonical order. The client displays these values and uses the supplied movement speed.

## Build and run

```bash
cd cyberia-client

# Development build (BUILD_MODE=DEBUG, localhost websocket URL)
make -f Web.mk clean && make -f Web.mk all

# Release build
make -f Web.mk clean && make -f Web.mk all BUILD_MODE=RELEASE
```

`BUILD_MODE` picks the websocket URL baked into the WASM — see
[Compile-time configuration](#compile-time-configuration). The Data Server URL
is not a build argument: the client reads it from its own command line at
startup.

The build is part of the Underpost Platform static + PWA pipeline; production deploys go through `underpost client` and `underpost deploy`.

### Output

```
bin/
  index.html    — Emscripten HTML container (from shell.html template)
  index.wasm    — Compiled WebAssembly module
  index.js      — Emscripten JS glue
  index.data    — Preloaded data bundle
```

---

## Compile-time configuration

### Server URLs

`WS_URL` in `src/config.h` holds both websocket endpoints. `BUILD_MODE` picks
one: DEBUG takes `ws://localhost:8081/ws`, RELEASE takes
`wss://server.cyberiaonline.com/ws`. `window.CYBERIA_WS_ORIGIN`, injected by
`wasm-driver.py`, overrides it at runtime.

The Data Server URL is a command line argument, not a build argument:

```
wasm-driver.py --data-server-url=<origin>
  -> window.CYBERIA_ARGV -> Module.arguments -> main(argc, argv)
```

`config_init()` reads `--data-server-url=<origin>` from `argv`. The argument is
required: the client asserts and stops if it is absent. There is no environment
fallback, no config file and no runtime override.

`wasm-driver.py` appends any other flag to the client argv. `fetch_init()` reads
two stream diagnostics:

| Flag                                  | Effect                                                                               |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| `--stream-disable=<mode>[,<mode>...]` | Turns off `audio`, `audio-network`, `audio-runtime`, `atlas` or `dynamic` streaming. |
| `--stream-profile`                    | Records stream events in `Module.streamTrace` (30,000-entry ring, read in DevTools). |

### Constants (`src/config.h`)

| Constant                    | Default        | Description                                                                                                      |
| --------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `CYBERIA_CLIENT_HINTS_CODE` | `cyberia-main` | Lookup key for the optional client-hints fetch (presentation override key — never an instance/server identifier) |
| `MAX_TEXTURE_CACHE_SIZE`    | `512`          | Atlas texture LRU cap                                                                                            |
| `MAX_LAYER_CACHE_SIZE`      | `256`          | ObjectLayer metadata LRU cap                                                                                     |
| `MAX_ATLAS_CACHE_SIZE`      | `256`          | Atlas metadata LRU cap                                                                                           |
| `DEFAULT_FRAME_DURATION_MS` | `100`          | Default animation frame duration                                                                                 |

---

## Persistent storage

`serial.c` uses Emscripten's IDBFS (IndexedDB File System) at `/persistent/` for:

- Player authentication token (EIP-712 signature).
- Local game settings (audio volume, key bindings).
- Cached ObjectLayer metadata (session warm-up cache).

No game state is persisted client-side; the authoritative server is the source of truth.
