← Technical Architecture
Technical Architecture ~19 min read 3,743 words

Salt & Steel: Networking Model

Document type: Technical Architecture — Canonical
Status: Canonical
Last updated: 2026-04
See also: Server Architecture | Client Architecture | Instance Management | Security and Integrity


Overview

Salt & Steel's networking model inherits Path of Exile's server-authoritative foundation and its dual-mode (Lockstep / Predictive) synchronization design, then extends both to cover a challenge PoE never faced: a single session can encompass two simultaneous combat contexts at different spatial scales — personal ARPG combat at character scale and naval combat at ship scale — and the client must handle both, sometimes concurrently, without visible seams.

The design goal is not just "low latency" — it is correct latency handling. The GURPS 3d6 bell curve creates a specific timing relationship between player input and visible outcomes. Active defense timing (Dodge, Parry, Block) is the most latency-sensitive element in the personal combat system: a player who presses the dodge key must receive believable visual feedback immediately, while the server resolves whether the dodge succeeds. Naval steering must feel responsive despite the ship's inherent momentum model. Both domains require careful attention to where prediction is safe and where server authority must be respected without compromise.


Transport Protocol

Salt & Steel uses TCP as its primary transport, following PoE's established approach. The rationale is identical:

  • Reliable ordered delivery is required. Combat state updates must arrive in order; a position update arriving after a death notification would produce incorrect visual state. Implementing reliable ordered delivery over UDP adds complexity equivalent to reimplementing TCP — without a compelling latency advantage at Salt & Steel's tick rates.
  • ARPG tick rates tolerate TCP. At 30 Hz land tick rate and 20 Hz sea tick rate, the game's update cadence is orders of magnitude slower than the scenarios (64-128 Hz FPS games) where UDP's lower head-of-line blocking risk provides meaningful benefit. TCP retransmission artifacts — a delayed packet causing a brief stall — are less perceptible at 30 Hz than at 128 Hz.
  • Congestion control is beneficial. Many players connect over asymmetric consumer internet links with upstream congestion. TCP's built-in congestion window management adapts to these conditions automatically.

TCP tuning: The connection uses TCP_NODELAY (disabling Nagle's algorithm) to prevent the OS from buffering small packets for up to 200ms before sending. At typical Salt & Steel packet sizes (50-500 bytes per update), Nagle's algorithm would introduce unacceptable artificial latency. Each tick's state update is transmitted immediately as its own segment.

Connection keepalive: A 15-second keepalive probe detects dead connections. Stale connections (no client response to keepalive probe within 30 seconds) are terminated and the player is returned to their last safe checkpoint. This prevents ghost connections from consuming server resources.


Dual Networking Modes

Following PoE's architecture, Salt & Steel supports both Lockstep and Predictive modes, selectable per-player based on their network conditions.

Lockstep Mode

The client does not advance any game state locally. Every player input is sent to the server; the client waits for server confirmation before rendering the result. The server's state is the only state.

Mechanism:

  1. Player presses attack key → client sends INPUT_ATTACK packet with target hint, timestamp, and current client tick counter
  2. Client begins anticipatory animation (wind-up phase) — this is cosmetic, not game state advancement
  3. Server receives input, resolves it in the next simulation tick, returns STATE_UPDATE with attack resolution
  4. Client receives result and completes animation accordingly (hit → impact; miss → whiff)
  5. Visual lag: one full RTT from input to confirmed visual result

When to use Lockstep: Recommended for players with RTT below 80ms to their selected server region. Above 80ms, the input latency becomes perceptible as sluggishness, especially during reactive defense timing. At 50ms RTT, the lag is 50ms — comparable to a 20 Hz monitor's frame latency. Below perception threshold for most players.

Lockstep gate: The client maintains a frame hold buffer. If the server acknowledgment has not arrived when the client's render frame is due, the client holds the rendered frame (renders the same frame twice) rather than advancing the animation. This is visible as a micro-stutter if packets are delayed. The stutter is honest — it correctly represents the uncertain game state — and is preferable to the alternative (advancing state incorrectly and then rubber-banding).

Predictive Mode

The client predicts the result of player actions and displays them immediately. The server resolves actions authoritatively and sends corrections when its result differs from the client's prediction.

Mechanism:

  1. Player presses attack key → client immediately begins full attack animation based on predicted hit
  2. Client sends INPUT_ATTACK packet to server simultaneously
  3. Server resolves attack in next tick, returns STATE_UPDATE
  4. If server result matches prediction: client continues animation cleanly
  5. If server result differs: client applies correction — typically a brief animation state snap (hit to miss or miss to hit) with a short reconciliation blend

Prediction accuracy targets:

  • Character movement: 95%+ accuracy with position error < 0.5m at 100ms RTT. Movement prediction is simply extrapolating current velocity — very reliable.
  • Basic attacks: 80%+ accuracy. Attack hit/miss prediction is based on client-side skill level and effective modifiers. The prediction is directionally correct most of the time; corrections are rare.
  • GURPS 3d6 roll outcomes: never predicted for display. The outcome of a die roll (hit/miss, defense success/failure, critical) is not shown until server confirmation arrives. The attack and defense animations are predicted; the resolution animation (hit impact, defense success flash) waits for server confirmation. This distinction preserves the integrity of the GURPS resolution without sacrificing input feel.

When to use Predictive: Mandatory recommendation for players above 100ms RTT. At high latency, Predictive provides a responsive feel that Lockstep cannot match. The trade-off (occasional visible correction) is acceptable given the alternative (obvious input lag).

Switching Between Modes

The networking mode can be changed in settings and takes effect at the next instance transition. It cannot be changed mid-instance (this would require a full client-server state reconciliation that is not implemented). The client displays the current mode and current RTT in the connection status panel.

During sea-to-land transitions (and vice versa), the networking mode is preserved — the player does not re-select mode on each transition.


Land Combat Synchronization

Tick Architecture

The land instance simulation server runs at 30-40 Hz (tick interval: 25-33ms). Each tick:

  1. Receive all pending input packets from connected clients
  2. Process NPC AI and pathfinding updates
  3. Resolve all queued combat interactions:
    • For each pending attack: look up attacker's effective skill, generate 3d6 roll, determine hit/miss/critical
    • For each pending defense attempt: look up defender's effective defense value, generate 3d6 roll, determine success/failure
    • Apply damage pipeline: raw damage → DR subtraction → wound modifier → HP deduction → status effect application
    • Apply FP costs for all ability uses
  4. Advance projectile positions (arrows, thrown weapons, spell bolts)
  5. Advance status effect timers (bleeding tick, poison progression, conditions)
  6. Advance monster AI state machines
  7. Generate state delta: all entities whose state changed this tick
  8. Transmit state delta to all connected clients

The state delta is per-client — each client only receives updates for entities within their area of interest (the player's visible area plus a 10m buffer zone). An entity at 300m range does not generate updates for this player.

GURPS 3d6 Roll Synchronization

The 3d6 roll is the most critical data element in Salt & Steel's combat. It must be:

  • Generated server-side (not inferable from any client-visible data)
  • Transmitted to the client efficiently
  • Never cached on the client for future use

Each combat resolution packet contains:

struct CombatResolutionPacket {
    uint32 attacker_entity_id;
    uint32 defender_entity_id;
    uint8  attack_roll_result;    // 3-18 (the die roll sum)
    uint8  effective_skill;       // effective skill target number
    bool   is_hit;
    bool   is_critical;
    uint8  hit_location;          // body location enum
    uint8  defense_type;          // DODGE / PARRY / BLOCK / NONE
    uint8  defense_roll_result;   // 3-18 (defense roll sum, if attempted)
    uint8  effective_defense;     // defense target number
    bool   defense_succeeded;
    uint16 raw_damage;
    uint8  damage_type;           // cutting / impaling / crushing / etc.
    uint16 penetrating_damage;    // after DR
    uint16 final_damage;          // after wound modifier
    uint8  status_effects_applied; // bitmask
}

This packet is 20 bytes — compact enough to include in the standard tick delta without special framing. The client displays the roll result in the combat log (Hit! [Skill 14 vs. Roll 11]) and plays the appropriate animation.

Active Defense Latency Handling

Active defense is the most latency-sensitive interaction in land combat. A player sees an attack animation, presses the dodge/parry/block key, and expects the defense to engage. The timing window for active defense in Salt & Steel corresponds to approximately 300ms from the start of an attack animation (the game-design specification; see combat design documents for rationale). Within this window, a defense input is valid.

Defense window implementation:

  • The server tracks the attack wind-up start time and the defense window close time for each attack entity
  • Client defense inputs timestamped with client-side clock, adjusted by measured RTT (client adjusts its clock based on round-trip measurement samples taken every 30 seconds)
  • Server validates: defense_received_time ≤ attack_window_close + max_allowed_latency_compensation
  • Maximum latency compensation: 100ms (half the target maximum RTT for Lockstep mode). This means a player at 200ms RTT can still successfully input a defense within the window, but the server treats a defense input arriving 100ms after the window as valid only if the client's timestamped send time was within the window.
  • Players above 200ms RTT will experience defense window failures that are genuinely caused by network conditions. The UI displays a "Defense window missed — check network connection" message when this is detected.

This approach prioritizes fairness for low-latency players while extending reasonable accommodation to moderate-latency players, rather than compensating all latency equally (which would benefit cheaters using artificial latency to extend defense windows).


Naval combat operates at ship scale — ships are the primary entities, not individual characters (except during boarding). The synchronization challenge is distinct from land combat: ships have significant momentum and turn slowly, making dead-reckoning prediction more reliable than character movement prediction, but also making corrections more visually jarring when they occur.

Ship State Synchronization

Each ship's state packet (sent from server to clients at 20 Hz):

struct ShipStatePacket {
    uint16 ship_entity_id;
    float32 world_x, world_y;     // position in sea instance
    float16 heading;              // 0-360 degrees, quantized
    float16 speed;                // knots, quantized
    float16 heel_angle;           // roll from wind pressure
    uint8   sail_state;           // enum: FULL / REEFED / FURLED + angle
    uint8   hull_integrity[4];    // bow/stern/port/starboard, 0-100
    bool    is_flooding;
    uint8   mast_state[3];        // per-mast: INTACT / DAMAGED / GONE
    uint8   port_broadside_ready; // reload %
    uint8   starboard_broadside_ready;
    uint8   crew_count;           // surviving crew, 0-255
    uint8   crew_morale;          // 0-100
}

At 20 Hz with up to 16 ships in an instance, the raw ship state bandwidth is:

  • 16 ships × 32 bytes × 20 updates/second = 10,240 bytes/second (10 KB/s) for ship state alone

This is comfortably within typical consumer internet upload/download capacity. Ship state is not delta-compressed (ships are almost always moving and the state changes each tick), but it is quantized: position is stored to 0.1m precision, heading to 0.35° precision — sufficient for visual rendering without bit-wasteful full 32-bit floats.

Ship Position Interpolation

Client-side ship rendering uses interpolation between the two most recently received state packets to smooth ship movement. At 20 Hz updates (50ms interval), naively snapping to each received position would produce visible jitter. Instead:

  • The client buffers the last two received ship state packets (100ms of history)
  • The current render frame interpolates linearly (for position) and spherically (for rotation) between these states
  • The interpolation playback point is held 100ms behind the current server time — this introduces 100ms of display lag but ensures smooth movement by always having "future" state to interpolate toward

For the player's own ship, interpolation is replaced by prediction during Predictive mode: the client advances the ship state forward based on the current steering inputs, then corrects to server state on each update. The steering of a ship is deterministic enough (physics model is simple — acceleration/deceleration curves based on wind and sail state) that client-side prediction is 90%+ accurate, keeping corrections minor.

Cannon Projectile Synchronization

Cannon projectiles (cannonballs, chain shot, grape shot) are simulated on the server. The server computes the trajectory including wind effects and sends the projectile's initial state to all clients:

struct ProjectileLaunchPacket {
    uint16 source_ship_id;
    uint8  cannon_type;           // CANNON / CHAIN / GRAPE / BAR
    float32 origin_x, origin_y, origin_z;  // world position
    float16 direction_h, direction_v;       // horizontal/vertical angles
    float16 muzzle_velocity;
    uint16  flight_time_ms;       // server-computed total flight time
    bool    is_impact_predicted;   // server has already computed landing
    float32 impact_x, impact_y;   // landing point (if predictable)
}

The client renders the projectile's flight as a visual element interpolated along the server-computed trajectory. The projectile's visual is purely cosmetic — it does not affect game state. The actual damage is applied server-side at impact time. The client receives a damage application packet simultaneously with the impact visuals.

For high-velocity cannonballs (fast enough that flight time is under one server tick), the server sends a combined launch-plus-impact packet. The client plays the launch flash, flight, and impact as a single animated sequence without waiting for separate packets.

Grape shot (short-range crew-suppressing fire) generates many small projectiles. Rather than sending 50 individual projectile packets, the server sends a single GRAPE_SHOT_VOLLEY packet with a spread pattern descriptor (angle, density, range). The client generates the visual particles from this descriptor on its own GPU — the display is approximated, but the actual crew casualties are server-computed and sent as damage packets.

Boarding Transition Handshake

Boarding is the most complex networking event in Salt & Steel: it transitions combat from ship-scale (20 Hz tick) to personal-scale (30 Hz sub-tick) while keeping both scales active simultaneously.

Boarding initiation sequence:

  1. Player inputs boarding command (within boarding range, crew morale sufficient)
  2. Server validates boarding conditions: range ≤ boarding threshold, relative velocity ≤ maximum boarding velocity, both ships have sufficient crew
  3. Server sends BOARDING_INITIATED packet to all clients in the sea instance — this packet includes the attacker ship ID, defender ship ID, boarding side (port/starboard), and a boarding engagement ID
  4. Both ships' clients receive the packet and play the grappling hook animation (cosmetic)
  5. Server spawns a boarding combat sub-context within the sea instance: a 30 Hz GURPS simulation running on the deck geometry of the defender's ship
  6. Crew members from both ships are assigned to the boarding context by the server; their prior ship roles are suspended
  7. The player's client receives a BOARDING_CONTEXT_JOIN packet with their spawn position on the defender's deck
  8. The client camera transitions from naval view to deck view over 2 seconds (during which the boarding animation plays)
  9. Both the boarding sub-context and the main sea-scale simulation continue running simultaneously — remaining crew members not in the boarding action continue firing cannons, and the two ships continue taking damage from each other's gunners

Simultaneous scale operation: The server maintains two update streams for a player in a boarding action:

  • The sea-scale update (20 Hz): ship hull status, enemy ship's remaining broadside, weather changes
  • The boarding sub-context update (30 Hz): GURPS combat state for boarding participants

The client receives and processes both streams. HUD shows a split-context indicator: the main HUD shows the player's personal combat state (HP, FP, active defenses), and a smaller overlay shows the player's ship's hull status and broadside reload.

Boarding end conditions:

  • All attackers killed or driven off → boarding fails, context dissolved, crew returned to ship roles
  • All defenders killed or surrendered → boarding succeeds, BOARDING_SUCCEEDED packet triggers ship capture sequence
  • Both ships begin sinking → BOARDING_ABORTED packet, all surviving crew must swim to safety or their ship

Weather State Synchronization

Weather state is owned by the Weather Service (running on the server infrastructure). It is injected into sea instance ticks as a parameter set — clients receive weather state as part of the periodic tick delta.

Weather parameters transmitted per-tick (when changed):

struct WeatherStatePacket {
    uint8  wind_direction;    // 0-255 mapped to 0-360°
    uint8  wind_speed;        // 0-255 mapped to 0-100 knots
    uint8  wave_height;       // 0-255 mapped to 0-20m
    uint8  wave_period;       // seconds, affects Tessendorf FFT input
    uint8  visibility;        // 0-255 mapped to 0-4km visibility
    uint8  rain_intensity;    // 0 = none, 255 = torrential
    uint8  fog_intensity;     // 0 = none, 255 = zero-vis fog
    uint8  storm_intensity;   // 0 = calm, 255 = hurricane
    bool   lightning_active;
    uint32 lightning_seed;    // drives client-side lightning position generation
}

Weather changes smoothly: the Weather Service advances weather state on a real-time clock, and the parameters transmitted each tick are interpolated toward the target state. Clients render the current weather parameters without further interpolation (the 20 Hz update is frequent enough that per-client weather interpolation is unnecessary at nautical scales).


Area of Interest Filtering

Not all entities in a sea instance are relevant to all players. Area of interest filtering reduces bandwidth and prevents information leakage (seeing enemy ship positions that shouldn't be visible through fog).

Land Instance AoI

  • Radius: 120m from player character (larger than the visible screen area)
  • Entities within AoI: receive full state updates each tick
  • Entities outside AoI: no updates sent
  • AoI edge: entities transitioning in/out of AoI receive spawn/despawn packets

Sea Instance AoI — Ship Scale

The sea instance AoI operates on a two-tier model:

  • Visibility tier (0-4km, modulated by weather visibility parameter): Ships within visibility range receive full position and state updates
  • Detection tier (4-8km, passive awareness): Ships outside visibility but within detection range receive coarse positional pings (position rounded to nearest 500m, 2 Hz update rate). This represents lookout awareness — you know roughly where a ship is but cannot see it precisely. Detection range is modified by the player's Lookout crew skill.
  • Beyond detection: No updates. These ships do not exist to this client's sea-scale AoI.

This two-tier model is both a performance optimization and a gameplay design element: fog reduces the visibility tier boundary (potentially to 0 in zero-visibility supernatural fog), making navigation and ambush genuinely possible at the simulation level.

AoI Transitions and Information Security

When a ship crosses from beyond-AoI into detection range, the client receives a spawn packet with the ship's general class and faction (enemy / neutral / unknown) but not detailed ship statistics. Detailed state packets begin arriving when the ship enters visibility range.

This prevents information exploitation: a client cannot determine enemy ship hull integrity, cannon loadout, or crew state beyond what would be observable in-world. The server enforces AoI — clients cannot request information for entities outside their AoI.


Latency Handling — Summary Table

Combat Element Server Authority Client Prediction Latency Compensation
Character position Final arbiter Extrapolate current velocity Server corrects via rubber-band (≤200ms visible)
Attack hit/miss Server 3d6 roll Animation predicted, result awaited 300ms attack window; 100ms defense compensation
Active defense success Server 3d6 roll Animation predicted, result awaited 100ms timestamp compensation for defense window
GURPS damage value Server-only Never predicted Damage applied visually on server confirmation
FP / HP values Server-only Never predicted HUD always shows confirmed server values
Ship position Server-authoritative Extrapolate physics model Smooth correction via blend (20 Hz updates)
Cannon projectile Server-simulated Visual trajectory is approximated Impact plays on server-confirmed time
Weather state Server-owned Client renders server parameters 20 Hz updates; visual changes are gradual
Boarding initiation Server-validated None 2-second animation covers round-trip
Crew combat in boarding Server GURPS sim Animation predicted, result awaited Same as character combat above

Packet Budget Analysis

Per-player per-second bandwidth at steady state:

Data Type Direction Bytes/sec (estimated)
Land instance tick updates (30 Hz) Server → Client 3,000-15,000
Player input packets Client → Server 200-500
Party member state (5 others at 30 Hz) Server → Client 2,000-6,000
AoI entity state (50-200 entities) Server → Client 5,000-25,000
Total land instance Bidirectional 10-46 KB/s
Sea instance tick (20 Hz, 8 ships) Server → Client 2,000-8,000
Player ship input Client → Server 100-200
Projectile packets (active combat) Server → Client 1,000-5,000
AoI ship updates (16 ships) Server → Client 5,000-10,000
Total sea instance Bidirectional 8-23 KB/s

These estimates are well within typical consumer broadband capabilities (2+ Mbps upload, 10+ Mbps download). Mobile network play is feasible under good signal conditions but not guaranteed — the game does not optimize for mobile data saving.


Party Synchronization

In a 6-player land party, each client receives the state of all other party members in addition to all NPC entities. Network load scales sub-linearly because area-of-interest filtering reduces updates for party members in different rooms.

Party join mid-instance: When a player joins a running instance via a ship anchor point or portal:

  1. Server generates a full world state snapshot of the current instance — all alive entities, their positions and current state, ground items, boss health, boss phase
  2. Snapshot is compressed (zlib) and transmitted — typical snapshot size for a mid-instance land area: 50-200 KB
  3. Client receives snapshot and initializes the scene in background while the loading screen is still visible
  4. Once snapshot is processed, the player is placed in the instance and visible to other clients
  5. Other clients receive a PLAYER_JOINED entity spawn packet and begin receiving that player's position updates

Protocol Security Considerations

All game protocol packets include:

  • A session token (encrypted, server-validated) confirming the player's authenticated identity
  • A sequence number for ordering and replay detection
  • A timestamp for defense window validation

Packets without valid session tokens are dropped at the gateway tier, before reaching simulation servers. The simulation server never processes unauthenticated inputs. See security-and-integrity.md for full security architecture.


Cross-references: design/10-technical-architecture/server-architecture.md, design/10-technical-architecture/client-architecture.md, design/10-technical-architecture/instance-management.md, design/10-technical-architecture/security-and-integrity.md