A plug-and-play solution for running GURPS combat, with tracking, VTT, dicerolling, and more
Fantactical docs NETWORKING.md
8.8 kB
Markdown
at main

Networking #

Fantactical uses a server-authoritative JSON-over-WebSocket protocol. The same desktop binary runs in off, server, or client mode; mode and connection details are command-line options rather than source edits.

Launch #

# Server (bind all interfaces when remote clients must connect)
cargo run --release -- \
  --network-mode server --host 0.0.0.0 --port 9002 --token replace-me

# Client
cargo run --release -- \
  --network-mode client --host 192.0.2.10 --port 9002 --token replace-me

The defaults are mode off, host 127.0.0.1, port 9002, and token fantactical. Use a non-default token for a real session.

Server mode binds its listener before entering Bevy's event loop. An invalid address or occupied port is reported on stderr and exits with status 1 instead of opening a window that cannot accept clients.

Authority model #

  • The server owns the canonical GameStateHistory, AppSettings, and EventLog.
  • A client starts with a valid empty encounter and does not import a bundled sample or load local session files.
  • Client UI events are forwarded as intents. Client battlemap, GM, phase, and persistence systems do not apply those changes locally.
  • The server checks protocol permissions, current phase, actor/target identity, attack/defense indices, and model legality before accepting an intent.
  • Accepted mutations create authoritative snapshots and are broadcast back to clients. There is no client-side prediction.

The first client admitted while the server has no connected clients becomes GM. Later clients start as non-GM with no actor ownership. The GM Config panel shows a live connected-client roster and can assign actor IDs to each non-GM client.

Wire protocol #

Definitions live in src/network/mod.rs.

ClientMessage (23 variants) #

Variant Important fields Purpose/permission
Auth token Must be the first WebSocket message
DeclareManeuver source/target/hex, maneuver, extra efforts Controlled actor or GM
ConfirmAttackSetup attacker, attack index, location, target Controlled attacker or GM
RequestRandomHitLocation attacker, attack index, target Controlled attacker or GM; server rolls the location
SelectDefense defender, DefenseTypeWire Controlled defender or GM
RollDice — GM-only generic roll request retained by the protocol
RequestRoll actor Phase-specific roll for controlled actor or GM
AdvancePhase actor Phase-specific continue/skip intent
CancelPhase actor Phase-specific cancel intent
AddModifier label, value, optional actor GM-only
RemoveModifier index, optional actor GM-only
Rewind — GM-only
SetPainThreshold actor, threshold GM-only
SetPosture actor, posture Controlled actor or GM
MoveActor actor, position Controlled actor or GM
SetFacing actor, six-way facing Controlled actor or GM
ReorderTurnOrder from/to slot GM-only
ShockToggle enabled GM-only setting mutation
ImportSheet JSON text GM-only server-side import
ReloadSheet actor, JSON text GM-only server-side replacement
RemoveActor actor GM-only
SetActorTurnConfig actor, turns, attacks, ETS GM-only; values must be nonzero
SetActorOwnership client, actor IDs GM-only

DefenseTypeWire is Dodge, Parry { attack_index }, or Block { attack_index }.

ServerMessage (10 variants) #

Variant Purpose
AuthSuccess Supplies client ID and GM flag
AuthFailure Rejects the handshake with a reason
StateSnapshot Full authoritative GameStateHistory
SettingsSnapshot Authoritative AppSettings
RollResult Roll label and total
LogEntry One appended log entry
EventLogSnapshot Full log for initial sync or non-append replacement
ActorOwnership Actor IDs the receiving client controls
ClientRoster Connected client IDs, GM flags, and assignments; sent to GMs
Error Rejected intent or server-side validation message

Server task #

spawn_server(ServerConfig) returns Bevy-facing outgoing/incoming Tokio channels and spawns a listener task.

For each connection:

  1. accept the WebSocket upgrade;
  2. require Auth as the first frame and compare the shared token;
  3. allocate a client ID and determine GM status;
  4. send AuthSuccess and an initial empty ActorOwnership message, and keep authenticated GMs' client rosters synchronized;
  5. forward authorized client messages to the Bevy bridge while delivering broadcasts or directed messages from Bevy;
  6. remove the client and notify Bevy on disconnect.

The server-side connection layer applies the role/ownership policy before a message reaches Bevy. The Bevy bridge checks it again and performs state-dependent validation.

Client task and reconnect #

spawn_client() returns outgoing/incoming channels. A Connect request stores host, port, and token, then opens ws://host:port/ and authenticates.

After a transport disconnect, the client retains the connection configuration and retries with exponential backoff from one second to a maximum of 30 seconds. An explicit Disconnect clears the desired connection. Authentication failure is reported rather than treated as a valid connection.

A reconnect is a new authenticated server connection and receives a new client ID. There is no resume credential, so prior GM status and actor ownership are not restored automatically.

NetworkState exposes connection status, client ID, GM flag, owned actor IDs, and the GM-visible client roster to the Bevy application. The present UI does not provide a dedicated network status/configuration screen.

Synchronization #

When a client connects, the server sends it:

  1. the current history;
  2. current settings;
  3. a full event-log snapshot;
  4. its actor ownership list.

Authenticated GMs additionally receive the live client roster whenever a client connects, disconnects, or its assignment changes.

Afterward:

  • changed history and settings are broadcast as snapshots. History snapshots include CombatResolution and defense windows, restoring the active combat prompt on initial sync, reconnect, or rewind;
  • append-only log growth is broadcast as individual LogEntry messages;
  • a full EventLogSnapshot is used when append-only continuity cannot be established;
  • portrait bytes remain in network state snapshots so remote portrait caches can hydrate. Disk history persistence separately omits embedded image bytes while retaining portrait/source paths.

Random-location requests contain only the attack context, never a client-made roll or selected location. The authoritative server validates that context, rolls the humanoid table and side die, and broadcasts the resulting ordinary state snapshot.

Facing changes follow the same authority boundary as token movement. The client previews the ring gesture only while the pointer is held, then sends a SetFacing intent. The server verifies actor ownership, pushes a new immutable snapshot only when the direction changed, and broadcasts that history. Rewind therefore restores the earlier facing without a separate presentation message.

Persistence interaction #

Only off and server roles write session history/log files. Client mode rejects --load-history and --load-log, skips periodic/exit persistence, and waits for authoritative synchronization.

Security and operational limits #

  • The transport is plain ws://, not TLS. The shared token and game data are not encrypted; use a trusted network or a secure tunnel/reverse proxy.
  • Authentication uses one shared token, with no account identity or token rotation.
  • The first connected client becomes GM automatically. If that GM disconnects while another client remains, the server does not currently promote a replacement.
  • Transport reconnect does not restore the previous client identity, GM role, or actor ownership.
  • Channels are unbounded and there is no rate limiting or production load testing.
  • There is no headless server binary or in-app connection setup/status screen.

Verification #

Protocol serialization/authorization has 28 unit tests; the Bevy network bridge has focused phase, facing, log-sync, roster, and ownership tests. Five async integration tests cover:

  • authentication and broadcast;
  • first/second-client GM assignment;
  • failed connection handling;
  • server enforcement and live actor-ownership updates;
  • reconnect with retained configuration.

Load, hostile-client, TLS/proxy, and long-running multi-client behavior remain manual QA areas.