Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

D060: Netcode Parameter Philosophy — Automate Everything, Expose Almost Nothing

Status: Settled Decided: 2026-02 Scope: ic-net, ic-game (lobby), D058 (console) Phase: Phase 5 (Multiplayer)

Decision Capsule (LLM/RAG Summary)

  • Status: Settled
  • Phase: Phase 5 (Multiplayer)
  • Canonical for: Netcode parameter exposure policy (what is automated vs player/admin-visible) and multiplayer UX philosophy for netcode tuning
  • Scope: ic-net, lobby/settings UI in ic-game, D058 command/cvar exposure policy
  • Decision: IC automates nearly all netcode parameters and exposes only a minimal, player-comprehensible surface, with adaptive systems handling most tuning internally.
  • Why: Manual netcode tuning hurts usability and fairness, successful games hide this complexity, and IC’s sub-tick/adaptive systems are designed to self-tune.
  • Non-goals: A comprehensive player-facing “advanced netcode settings” panel; exposing internal transport/latency/debug knobs as normal gameplay UX.
  • Invariants preserved: D006 pluggable netcode architecture remains intact; automation policy does not prevent internal default changes or future netcode replacement.
  • Defaults / UX behavior: Players see only understandable controls (e.g., game speed where applicable); admin/operator controls remain narrowly scoped; developer/debug knobs stay non-player-facing.
  • Security / Trust impact: Fewer exposed knobs reduces misconfiguration and exploit/abuse surface in competitive play.
  • Performance / Ops impact: Adaptive tuning lowers support burden and avoids brittle hand-tuned presets across diverse network conditions.
  • Public interfaces / types / commands: D058 cvar/command exposure policy, lobby parameter surfaces, internal adaptive tuning systems (see body for exact parameters)
  • Affected docs: src/03-NETCODE.md, src/17-PLAYER-FLOW.md, src/06-SECURITY.md, src/decisions/09g-interaction.md
  • Revision note summary: None
  • Keywords: netcode parameters, automate everything, expose almost nothing, run-ahead, command delay, tick rate, cvars, multiplayer settings

Context

Every lockstep RTS has tunable netcode parameters: tick rate, command delay (run-ahead), game speed, sync check frequency, stall policy, and more. The question is which parameters to expose to players, which to expose to server admins, and which to keep as fixed engine constants.

This decision was informed by a cross-game survey of configurable netcode parameters — covering both RTS (C&C Generals, StarCraft/Brood War, Spring Engine, 0 A.D., OpenTTD, Factorio, Age of Empires II, original Red Alert) and FPS (Counter-Strike 2) — plus analysis of IC’s own sub-tick and adaptive run-ahead systems.

The Pattern: Successful Games Automate

Every commercially successful game in the survey converged on the same answer: automate netcode parameters, expose almost nothing to players.

Game / EnginePlayer-Facing Netcode ControlsAutomatic SystemsOutcome
C&C Generals/ZHGame speed onlyAdaptive run-ahead (200-sample rolling RTT + FPS), synchronized RUNAHEAD commandPlayers never touch latency settings; game adapts silently
FactorioNone (game speed implicit)Latency hiding (always-on since 0.14.0, toggle removed), server never waits for slow clientsRemoved the only toggle because “always on” was always better
Counter-Strike 2NoneSub-tick always-on; fixed 64 Hz tick (removed 64/128 choice from CS:GO)Removed tick rate choice because sub-tick made it irrelevant
AoE II: DEGame speed onlyAuto-adapts command delay based on connection qualityNo exposed latency controls in ranked
Original Red AlertGame speed onlyMaxAhead adapts automatically every 128 frames via host TIMING eventsPlayers never interact with MaxAhead; formula-driven
StarCraft: Brood WarGame speed + latency setting (Low/High/Extra High)None (static command delay per setting)Latency setting confuses new players; competitive play mandates “Low Latency”
Spring EngineGame speed (host) + LagProtection mode (server admin)Dynamic speed adjustment based on CPU reporting; two speed control modesMore controls → more community complaints about netcode
0 A.D.NoneNone (hardcoded 200ms turns, no adaptive run-ahead, stalls for everyone)Least adaptive → most stalling complaints

The correlation is clear: games that expose fewer netcode controls and invest in automatic adaptation have fewer player complaints and better perceived netcode quality. Games that expose latency settings (BW) or lack automatic adaptation (0 A.D.) have worse player experiences.

Decision

IC adopts a three-tier exposure model for netcode parameters:

Tier 1: Player-Facing (Lobby GUI)

SettingValuesDefaultWho SetsScope
Game SpeedSlowest / Slower / Normal / Faster / FastestSlower (~15 tps)Host (lobby)Synced — all clients

One setting. Game speed is the only parameter where player preference is legitimate (“I like slower, more strategic games” vs. “I prefer fast-paced gameplay”). In ranked play, game speed is server-enforced and not configurable (pending decision P009 — whether one canonical speed applies globally or communities may choose with rating normalization).

Game speed affects only the interval between sim ticks — system behavior is tick-count-based, so all game logic works identically at any speed. Single-player can change speed mid-game; multiplayer sets it in lobby. This matches how every C&C game handled speed (see 02-ARCHITECTURE.md § Game Speed).

Mobile tempo advisor compatibility (D065): Touch-specific “tempo comfort” recommendations are client/UI advisory only. They may highlight a recommended band (slower-normal, etc.) or warn a host that touch players may be overloaded, but they do not create a new authority path for speed selection. The host/queue-selected game speed remains the only synced value, and ranked speed remains server-enforced.

Tier 2: Advanced / Console (Power Users, D058)

Available via console commands or config.toml. Not in the main GUI. Flagged with appropriate cvar flags:

CvarTypeDefaultFlagsWhat It Does
net.sync_frequencyint120SERVERTicks between full state hash checks
net.desync_debug_levelint0DEV_ONLY0-3, controls desync diagnosis overhead (see 03-NETCODE.md § Debug Levels)
net.show_diagnosticsboolfalsePERSISTENTToggle network overlay (latency, jitter, packet loss, tick timing)
net.visual_predictionbooltrueDEV_ONLYClient-side visual prediction; disabling useful only for testing perceived latency
net.simulate_latencyint0DEV_ONLYArtificial one-way latency in ms (debug builds only)
net.simulate_lossfloat0.0DEV_ONLYArtificial packet loss percentage (debug builds only)
net.simulate_jitterint0DEV_ONLYArtificial jitter in ms (debug builds only)

These are diagnostic and testing tools, not gameplay knobs. The DEV_ONLY flag prevents them from affecting ranked play. The SERVER flag on sync_frequency ensures all clients use the same value.

Tier 3: Engine Constants (Not Configurable at Runtime)

ParameterValueWhy Fixed
Sim tick rateSet by game speed preset (Slowest 80ms / Slower 67ms / Normal 50ms / Faster 35ms / Fastest 20ms per tick; Slower is default ≈ 15 tps)The sim does identical work per tick regardless of speed — game speed changes only how frequently ticks are scheduled in wall time. The Slower default (~15 wall-time tps) is the performance budget target for weak machines. In lockstep, ticks are synchronization barriers (collect orders → process → advance sim → exchange hashes). Higher base rates would multiply CPU cost (full ECS update per tick for 500+ units), network overhead, and late-arrival risk with no gameplay benefit. Visual interpolation makes even ~15 wall-time tps smooth at 60+ FPS render. See 03-NETCODE.md § “Why Sub-Tick Instead of a Higher Tick Rate”
Sub-tick orderingAlways onZero cost (~4 bytes/order + one sort of ≤5 items); produces visibly fairer outcomes in simultaneous-action edge cases; CS2 proved universal acceptance; no reason to toggle
Adaptive run-aheadAlways onGenerals proved this works over 20 years; adapts to both RTT and FPS; synchronized via network command
Timing feedbackAlways onClient self-calibrates order submission timing based on relay feedback; DDNet-proven pattern
Match QoS auto-profileAlways onRelay calibrates deadline/run-ahead at match start and adapts within bounded profiles. Gives better feel under real-world lag while preserving match-wide fairness semantics
Stall policyNever stall (relay drops late orders)Core architectural decision; stalling punishes honest players for one player’s bad connection
Anti-lag-switchAlways onRelay owns the clock; non-negotiable for competitive integrity
Visual predictionAlways onFactorio lesson — removed the toggle in 0.14.0 because always-on was always better; cosmetic only (sim unchanged)

Sub-Tick Is Not Optional

Sub-tick order fairness (D008) is always-on — not a configurable feature:

  • Cost: ~4 bytes per order (sub_tick_time: u32) + one stable sort per tick of the orders array (typically 0-5 orders — negligible).
  • Benefit: Fairer resolution of simultaneous events (engineer races, crate grabs, simultaneous attacks). “I clicked first, I won” matches player intuition.
  • Player experience: The mechanism is automatic (players don’t configure timestamps), but the outcome is very visible — who wins the engineer race, who grabs the contested crate, whose attack order resolves first. These moments define close games. Without sub-tick, ties are broken by player ID (always unfair to higher-numbered players) or packet arrival order (network-dependent randomness). With sub-tick, the player who acted first wins. That’s a gameplay experience players notice and care about.
  • If made optional: Would require two code paths in the sim (sorted vs. unsorted order processing), a deterministic fallback that’s always unfair to higher-numbered players (player ID tiebreak), and a lobby setting nobody understands. Ranked would mandate one mode anyway. CS2 faced zero community backlash — no one asked for “the old random tie-breaking.”

Match QoS Auto-Profile (Fairness + Feel)

To avoid the “first 1-2 seconds feel wrong” problem and to improve playability under moderate lag, relay matches use an always-on Match QoS auto-profile:

  1. Pre-match calibration during loading: collect short RTT/jitter samples for each player.
  2. Profile-bounded initialization: derive one shared tick_deadline and one shared run_ahead for the match from the measured envelope, then clamp to policy bounds.
  3. Bounded in-match adaptation: increase quickly on sustained late arrivals, decrease slowly after sustained stability (hysteresis).
  4. Per-player assist only for submission timing: clients may use per-player send offsets, but fairness semantics stay match-global.

The key guardrail is deliberate:

  • Never per-player fairness rules. Intra-tick ordering remains one canonical relay rule for all players.
  • Only per-player delivery assist. Individual timing offsets influence when a client sends, not how the relay ranks contested actions.

Recommended policy envelopes:

Queue Typetick_deadline_ms envelopeShared run-ahead envelopeGoal
Ranked / Competitive90-1403-5 ticksTight fairness and responsiveness; strict anti-abuse posture
Casual / Community120-2204-7 ticksBetter tolerance for mixed/WiFi links; fewer late-drop frustrations

These are policy bounds, not player settings. The relay auto-selects/adjusts inside them.

At 30 tps (tick_interval_ms ≈ 33.33), these run-ahead envelopes are derived from ceil(tick_deadline_ms / tick_interval_ms) so delay and scheduling budgets stay internally consistent.

Default QoS adaptation constants (per queue profile):

ConstantRanked / CompetitiveCasual / Community
Initialization one-way clip (one_way_clip_us)25 ms80 ms
Initialization jitter clip (jitter_clip_us)12 ms40 ms
Safety margin (safety_margin_us)8 ms15 ms
EWMA alpha (ewma_alpha_q15)0.200.25
Raise threshold (raise_late_rate_bps)2.0%3.5%
Raise windows (raise_windows)32
Lower threshold (lower_late_rate_bps)0.2%0.5%
Lower windows (lower_windows)86
Cooldown windows (cooldown_windows)21
Per-player influence cap (per_player_influence_cap_bps)40%60%

Operational guardrails:

  • Outlier-resistant initialization: Use clipped percentile aggregation at match start, not raw worst-player values.
  • Abuse resistance: A player with repeated late bursts that do not correlate with RTT/jitter/loss cannot unilaterally push global delay up.
  • Auditability: Every QoS adjustment is recorded to replay metadata and relay telemetry.
  • Player feedback: Missed-deadline outcomes should be surfaced with concise in-game indicators to reduce perceived unfairness.

Rationale

Netcode parameters are not like graphics settings. Graphics preferences are subjective (some players prefer performance over visual quality). Netcode parameters have objectively correct values — or correct adaptive algorithms. Exposing the knob creates confusion:

  1. Support burden: “My game feels laggy” → “What’s your tick rate set to?” → “I changed some settings and now I don’t know which one broke it.”
  2. False blame: Players blame netcode settings when the real issue is their WiFi or ISP. Exposing knobs gives them something to fiddle with instead of addressing the root cause.
  3. Competitive fragmentation: If netcode parameters are configurable, tournaments must mandate specific values. Different communities pick different values. Replays from one community don’t feel the same on another’s settings.
  4. Testing matrix explosion: Every configurable parameter multiplies the QA matrix. Sub-tick on/off × 5 sync frequencies × 3 debug levels = 30 configurations to test.

The games that got this right — Generals, Factorio, CS2 — all converged on the same philosophy: invest in adaptive algorithms, not exposed knobs.

Alternatives Considered

  • Expose tick rate as a lobby setting (rejected — unlike game speed, tick rate affects CPU cost, bandwidth, and netcode timing in ways players can’t reason about. If 30 tps causes issues on low-end hardware, that’s a game speed problem (lower speed = lower effective tps), not a tick rate problem.)
  • Expose latency setting like StarCraft BW (rejected — BW’s Low/High/Extra High was necessary because the game had no adaptive run-ahead. IC has adaptive run-ahead from Generals. The manual setting is replaced by a better automatic system.)
  • Expose sub-tick as a toggle (rejected — see analysis above. Zero-cost, always-fairer, produces visibly better outcomes in contested actions, CS2 precedent.)
  • Expose everything in “Advanced Network Settings” panel (rejected — the Spring Engine approach. More controls correlate with more complaints, not fewer.)

Integration with Existing Decisions

  • D006 (Pluggable Networking): The NetworkModel trait encapsulates all netcode behavior. Parameters are internal to each implementation, not exposed through the trait interface. LocalNetwork ignores network parameters entirely (zero delay, no adaptation needed). RelayLockstepNetwork manages run-ahead, timing feedback, and anti-lag-switch internally.
  • D007 (Relay Server): The relay’s tick deadline, strike thresholds, and session limits are server admin configuration, not player settings. These map to relay config files, not lobby GUI.
  • D008 (Sub-Tick Timestamps): Explicitly non-optional per this decision.
  • D015 (Efficiency-First Performance): Adaptive algorithms (run-ahead, timing feedback) are the “better algorithms” tier of the efficiency pyramid — they solve the problem before reaching for brute-force approaches.
  • D033 (Toggleable QoL): Game speed is the one netcode-adjacent setting that fits D033’s toggle model. All other netcode parameters are engineering constants, not user preferences.
  • D058 (Console): The net.* cvars defined above follow D058’s cvar system with appropriate flags. The diagnostic overlay (net_diag) is a console command, not a GUI setting.