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, andEventLog. - 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:
- accept the WebSocket upgrade;
- require
Authas the first frame and compare the shared token; - allocate a client ID and determine GM status;
- send
AuthSuccessand an initial emptyActorOwnershipmessage, and keep authenticated GMs' client rosters synchronized; - forward authorized client messages to the Bevy bridge while delivering broadcasts or directed messages from Bevy;
- 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:
- the current history;
- current settings;
- a full event-log snapshot;
- 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
CombatResolutionand defense windows, restoring the active combat prompt on initial sync, reconnect, or rewind; - append-only log growth is broadcast as individual
LogEntrymessages; - a full
EventLogSnapshotis 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.