docs(multiplayer): consolidate tracking into one document

multiplayer-todo.md and multiplayer-next.md tracked overlapping
information in two places. Fold everything into multiplayer-next.md
(architecture decisions, wire format, task breakdown with checkboxes,
gotchas list, testing notes) and delete multiplayer-todo.md. Section
numbers are unchanged, so existing code comments citing them by
section/task number still resolve; update every such reference to
point at the new filename.
This commit is contained in:
Josh Creek
2026-09-01 12:32:43 +01:00
parent 9dc1cc2d6f
commit 4fb7ddfecf
31 changed files with 1414 additions and 1660 deletions
+2 -3
View File
@@ -14,8 +14,7 @@ Because the gameplay concept (vehicle soccer) can't be copyrighted but specific
The prose docs carry far more design rationale than the code comments, and several are load-bearing:
- `multiplayer-next.md`**the current** multiplayer checklist. Short. Read this first for "what's left".
- `multiplayer-todo.md` — 250 KB of historical design decisions, per-task implementation evidence, and §9's numbered "gotchas" list. Code comments cite it constantly by section/task number (`§2.4`, `task 5.10`); when a comment does, that section is the real explanation. Mostly an archive: Phases 06 are done, and day-to-day work is tracked in `multiplayer-next.md` instead. The exception is a *new phase* — Phase 7 (Steam) and Phase 8 (matchmaking) both keep their numbered task breakdown and acceptance criteria in §7, because that is the format tasks are picked up from.
- `multiplayer-next.md`**the single multiplayer tracking document**: architecture decisions, the wire format, implementation evidence, a numbered "gotchas" list (§9), and the current task breakdown with checkboxes, all in one file. Start at §0 for "what's left". Code comments cite it constantly by section/task number (`§2.4`, `task 5.10`); when a comment does, that section is the real explanation. Phases 06 are done and mostly archival; day-to-day work is Phase 7 (Steam) and Phase 8 (matchmaking), whose numbered task breakdown and acceptance criteria live in §7, because that is the format tasks are picked up from.
- `TRAINING.md` — the full RL workflow (training, curriculum generations, export, eval, difficulty tiers).
- `SERVER.md` — dedicated-server build, config, systemd deploy, sizing.
- `STEAM.md` — optional GodotSteam custom-build setup and the transport contract.
@@ -108,7 +107,7 @@ GODOT_BIN=/path/to/godot make verify-enet-integration # non-default Godot b
| `tests/networked_match_smoke.tscn` | see its header | Shorter attended variant of the above. |
| `tests/net_sim_smoke.tscn` | see its header | The `--net-sim-*` latency/loss decorator actually changes observed behaviour. |
See `network_manager.gd`'s header comment and `multiplayer-todo.md` §9 gotchas 2530 for the non-obvious Godot/ENet failure modes these caught (`OfflineMultiplayerPeer` sentinel, premature peer teardown, `change_scene_to_file` off the real `current_scene`, unbounded `connection_failed`, the `is_client`-before-actually-connected race, `load()` not returning null on a broken script).
See `network_manager.gd`'s header comment and `multiplayer-next.md` §9 gotchas 2530 for the non-obvious Godot/ENet failure modes these caught (`OfflineMultiplayerPeer` sentinel, premature peer teardown, `change_scene_to_file` off the real `current_scene`, unbounded `connection_failed`, the `is_client`-before-actually-connected race, `load()` not returning null on a broken script).
**`main_menu.tscn`'s Host/Join flow** is verified the same way but needs a temporary autoload since it's the real main scene, not a wrapper: add `MainMenuTestHooks="*res://tests/main_menu_test_hooks.gd"` to `project.godot [autoload]`, run `godot --headless --path Game res://scenes/main_menu.tscn -- --role=<host|join_ok|join_refused|join_cancel>` (host first, sleep ~1s, then the join role), then remove the autoload line again — it must never ship registered.
+2 -2
View File
@@ -1,7 +1,7 @@
class_name InputJitterBuffer
extends RefCounted
# Per-player server-side input state (multiplayer-todo.md §3, task 3.2).
# Per-player server-side input state (multiplayer-next.md §3, task 3.2).
# Deliberately a standalone RefCounted with no scene/RPC dependency — same
# reason net_codec.gd and net_interpolator.gd are pure classes — so task
# 3.5's unit tests can drive it with scripted arrival traces with no live
@@ -18,7 +18,7 @@ extends RefCounted
# class's, since only the caller knows the current server tick.
const RING_SIZE := 32
# 500ms at 60Hz (multiplayer-todo.md §3.2's own numbers) — a duration, not a
# 500ms at 60Hz (multiplayer-next.md §3.2's own numbers) — a duration, not a
# tick-rate-derived constant, so left as a literal rather than pulling in
# SimConstants for one number.
const STARVE_ZERO_TICKS := 30
+1 -1
View File
@@ -1,7 +1,7 @@
class_name InputLeadController
extends RefCounted
# Client-owned input_lead control loop (multiplayer-todo.md §3.3, task 3.3).
# Client-owned input_lead control loop (multiplayer-next.md §3.3, task 3.3).
# Standalone RefCounted, same reason as input_jitter_buffer.gd — scene-free
# so it's directly unit-testable against scripted depth traces.
#
+1 -1
View File
@@ -9,7 +9,7 @@ extends Control
# of something else: change_scene_to_file() operates on
# get_tree().current_scene, and _on_disconnected_from_server()/_leave()
# below call it themselves, which hangs if this scene isn't actually the
# tree's current_scene when that happens (see multiplayer-todo.md §9
# tree's current_scene when that happens (see multiplayer-next.md §9
# gotcha 27 — found the hard way while building tests/lobby_smoke.gd).
@onready var _status_label: Label = %StatusLabel
+1 -1
View File
@@ -3,7 +3,7 @@ extends RefCounted
const NetBodyState = preload("res://scripts/net_body_state.gd")
# Client-owned local-ship prediction history (multiplayer-todo.md §4.3).
# Client-owned local-ship prediction history (multiplayer-next.md §4.3).
# This is deliberately independent of NetworkedMatch and the scene tree so
# sequence/ring behaviour can be tested from scripted traces. Each entry is
# tagged with its full sequence number: an old value in a wrapped slot is
+1 -1
View File
@@ -1,7 +1,7 @@
extends Node
# Autoload (project.godot [autoload] MatchNet). Handshake + roster layer on
# top of NetworkManager's raw transport (§2.5, §1.3 of multiplayer-todo.md).
# top of NetworkManager's raw transport (§2.5, §1.3 of multiplayer-next.md).
# hello/welcome, strict protocol_version and physics_ticks_per_second
# gating, player_joined/player_left, and — since lobby.tscn (task 1.5) needs
# somewhere durable to keep it across the lobby→match scene transition —
+1 -1
View File
@@ -41,7 +41,7 @@ signal match_bootstrap_received(state: int, at_tick: int, score: Dictionary, end
# §6.3 task 5.8: a spectator has been given a vacated slot at a kickoff.
signal slot_assigned_received(peer_id: int, slot_index: int)
# Input validation (multiplayer-todo.md §3.1 steps 2-3, task 3.4). Deliberately
# Input validation (multiplayer-next.md §3.1 steps 2-3, task 3.4). Deliberately
# lives here rather than in NetworkedMatch: framing/rate abuse is a protocol-
# level concern independent of any particular match's roster/slot state, and
# this autoload already owns the RPC that receives the raw bytes.
+1 -1
View File
@@ -1,6 +1,6 @@
class_name MatchState
# Match lifecycle states (multiplayer-todo.md §6.1, task 5.1).
# Match lifecycle states (multiplayer-next.md §6.1, task 5.1).
#
# Pure data + a transition table, deliberately with no scene, RPC or
# NetworkedMatch dependency — same reason net_codec.gd and
+1 -1
View File
@@ -1,6 +1,6 @@
extends RefCounted
# Plain data holder for one body's snapshot state (§2.4 of multiplayer-todo.md).
# Plain data holder for one body's snapshot state (§2.4 of multiplayer-next.md).
# Deliberately not Ship/Ball themselves, and deliberately not a scene-tree
# node — NetCodec's pack/unpack must stay callable from pure-function tests
# with no live scene. Phase 2's snapshot writer fills one of these per body
+2 -2
View File
@@ -1,7 +1,7 @@
class_name NetCodec
# Wire-format constants, quantisers, and pack/unpack for the two hot-path
# packets (§2 of multiplayer-todo.md). Pure functions only — no networking,
# packets (§2 of multiplayer-next.md). Pure functions only — no networking,
# no autoload state — so they're testable head-on by tests/test_runner.tscn
# without a live connection.
#
@@ -48,7 +48,7 @@ const BODY_FLAG_STALLED := 1 << 5
const BODY_FLAG_QUAT_W_SIGN := 1 << 6
# --- Quantisation ranges (§2.4 — derived from arena/gameplay constants, not
# restated prose; see multiplayer-todo.md for the ArenaBoundary/Ship/Ball
# restated prose; see multiplayer-next.md for the ArenaBoundary/Ship/Ball
# constants these are sized against) ---
const POS_RANGE := 64.0 # metres, ±
const VEL_RANGE := 64.0 # m/s, ±
+1 -1
View File
@@ -3,7 +3,7 @@ extends RefCounted
# Buffers recent snapshot samples for ONE remote body and produces
# interpolated states at any requested (possibly fractional) server tick —
# used twice per body (multiplayer-todo.md §4.1/§4.6, "dual-time remote
# used twice per body (multiplayer-next.md §4.1/§4.6, "dual-time remote
# entities"): once at the present-time estimate for the collider, once
# further back at present-minus-INTERP_DELAY for $Visual.
#
+1 -1
View File
@@ -1,6 +1,6 @@
extends RefCounted
# Local-ship reconciliation policy (multiplayer-todo.md §4.4). Kept out of
# Local-ship reconciliation policy (multiplayer-next.md §4.4). Kept out of
# NetworkedMatch so the decision table is pure-testable; the imperative half
# only writes Ship's existing Jolt-safe queued correction hooks.
+1 -1
View File
@@ -89,7 +89,7 @@ func _schedule(dispatch: Callable, target_peer_id: int, delay_sec: float) -> voi
return
# process_always = true: a simulated wire delay must keep counting down
# even if the local SceneTree pauses (match_mode.gd's goal-pause does
# this today; multiplayer-todo.md §8 already flags get_tree().paused
# this today; multiplayer-next.md §8 already flags get_tree().paused
# stopping the client's own send/receive loop as a separate refactor
# item). Pausing this timer too would let a paused client's in-flight
# packets pile up and arrive in a burst on unpause instead of on their
+2 -2
View File
@@ -3,7 +3,7 @@ extends Node
# Autoload (project.godot [autoload] NetworkManager). Owns transport-neutral
# hosting, joining, shutdown, and connection-state signals. Lives
# at a fixed autoload path so RPC NodePaths never depend on which scene is
# loaded (§1.3 of multiplayer-todo.md's derived decisions).
# loaded (§1.3 of multiplayer-next.md's derived decisions).
#
# server_relay = false is set the moment a peer exists: the default `true`
# lets any client rpc() any other client *through the server*, which this
@@ -25,7 +25,7 @@ extends Node
# pays the same tax again. set_multiplayer_poll_enabled(false) below turns
# that off; every caller that sends or expects to receive on a tight cadence
# must now call NetworkManager.poll() itself. The intended placement per
# multiplayer-todo.md §7 task 1.3 (client: end of _physics_process after
# multiplayer-next.md §7 task 1.3 (client: end of _physics_process after
# sending input, plus top of both _process and _physics_process for receive;
# server: tick start to drain, tick end to flush) has no real per-tick caller
# yet — that lands with the input/snapshot pipeline (tasks 1.4+, Phase 2-3).
+1 -1
View File
@@ -5,7 +5,7 @@ extends CanvasLayer
# Performance monitors; never touches rendering or gameplay state. Exists so
# 0.17/0.17b's graphics presets and resolution scaling are self-diagnosing —
# TIME_PROCESS vs total frame time tells the player whether they're CPU- or
# GPU-bound. See multiplayer-todo.md task 0.20.
# GPU-bound. See multiplayer-next.md task 0.20.
# ~2s of history at 60 fps; enough to make p50/p99 meaningful without the
# history itself being a rate-dependent quantity.
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ReplayLog
extends RefCounted
# Append-only binary server replay log (multiplayer-todo.md task 5.10).
# Append-only binary server replay log (multiplayer-next.md task 5.10).
#
# The highest-value debuggability investment in Phase 5, and cheap precisely
# because the packets are ALREADY flat bytes: this stores them verbatim rather
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerConfig
extends RefCounted
# Dedicated-server configuration (multiplayer-todo.md task 6.3): one
# Dedicated-server configuration (multiplayer-next.md task 6.3): one
# declaration of every server flag, one parser, one `--help`.
#
# Standalone RefCounted with no scene or RPC dependency — same reason as
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerLog
extends RefCounted
# Structured server logging (multiplayer-todo.md task 6.4).
# Structured server logging (multiplayer-next.md task 6.4).
#
# Extracted from server_boot.gd's private `_log`, which could only ever see
# what the boot scene itself observed: connects, disconnects, roster changes
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerMatchLoop
extends Node
# The dedicated server's match loop (multiplayer-todo.md task 6.5).
# The dedicated server's match loop (multiplayer-next.md task 6.5).
#
# THIS CLOSES A GAP NO TASK OWNED. Task 6.2 asks for "the exported binary runs
# a full match headless", but nothing in the product ever started a match:
+2 -2
View File
@@ -145,7 +145,7 @@ func queue_teleport_with_velocity(to: Transform3D, new_linear_velocity: Vector3,
_has_pending_teleport = true
# --- Netcode correction hooks (Phase 4; see multiplayer-todo.md §4.4) ---
# --- Netcode correction hooks (Phase 4; see multiplayer-next.md §4.4) ---
# Both stay zero until Phase 4 wires a reconciliation pass in, so the guarded
# hook in _integrate_forces below is a no-op today.
# Velocity delta from a soft correction, consumed once then cleared —
@@ -222,7 +222,7 @@ var _engine_lights: Array[OmniLight3D] = []
# All rendered geometry (hull, canopy, engine cores/flames/lights, Nose,
# TailFin) parents under this instead of the RigidBody3D directly, so a
# future prediction correction (task 0.14) can offset the visual without
# moving the collider — see multiplayer-todo.md task 0.2. CollisionShape3D
# moving the collider — see multiplayer-next.md task 0.2. CollisionShape3D
# and the controller child correctly stay on the body itself.
@onready var visual: Node3D = $Visual
+1 -1
View File
@@ -4,7 +4,7 @@ class_name SimConstants
# constant derived from "60 Hz" (Ship._tick_scaled's decay reference,
# reaction_ticks' export range, TrainingMode.TICKS_PER_SIM_SECOND) reads this
# instead of restating the literal, so changing it changes every derived
# constant coherently — see multiplayer-todo.md §5.6 on why a future 120 Hz
# constant coherently — see multiplayer-next.md §5.6 on why a future 120 Hz
# simulation needs to be a config change plus a retrain, not a protocol
# rewrite hunting down bare 60s.
#
+4 -4
View File
@@ -24,7 +24,7 @@ extends Node
# independently of stretch mode, since it scales the 3D viewport's own internal
# resolution before this blit rather than the window itself. Task 0.15b also
# found an unexplained ~6% non-uniform width scaling on this project's one
# tested (Mac/Retina) machine — see multiplayer-todo.md §5.5.1 — which needs
# tested (Mac/Retina) machine — see multiplayer-next.md §5.5.1 — which needs
# understanding before stretch mode is touched, not blindly carrying into a
# resolution-dependent change.
#
@@ -49,7 +49,7 @@ const SETTINGS_PATH := "user://settings.cfg"
# preset -> bundle applied to the individual fields below. CUSTOM has no
# bundle: selecting it just stops future preset changes from overwriting
# whatever the individual fields currently hold. Task 0.15b's measured
# per-effect costs (multiplayer-todo.md §5.5.1) were too noisy to rank these
# per-effect costs (multiplayer-next.md §5.5.1) were too noisy to rank these
# against each other, so each rung is "meaningfully fewer full-screen passes
# than the one above it" rather than a precisely tuned ladder.
const PRESET_BUNDLES := {
@@ -78,7 +78,7 @@ var shadows_enabled: bool = true
var glow_enabled: bool = true
# FXAA alone, not MSAA_FXAA: 4x MSAA *and* FXAA stacked is redundant blur for
# most scenes and costs more than either alone (see multiplayer-todo.md 0.19).
# most scenes and costs more than either alone (see multiplayer-next.md 0.19).
var aa_mode: AAMode = AAMode.FXAA
var glow_scale: float = 1.0
var brightness: float = 1.0
@@ -252,7 +252,7 @@ func apply_fps_cap() -> void:
# Called once by each arena's _ready() (and again on settings_changed, so an
# already-loaded arena updates live) to fold the user's glow/brightness
# preference into that arena's own baked Environment tuning, and to gate the
# preset-controlled full-screen passes (§5.5 of multiplayer-todo.md).
# preset-controlled full-screen passes (§5.5 of multiplayer-next.md).
func apply_to_environment(env: Environment) -> void:
if env == null:
return
+1 -1
View File
@@ -97,7 +97,7 @@ func test_snapshot_roundtrip_seven_bodies() -> void:
var packet := NetCodec.pack_snapshot(555, -2, 1234, segment)
assert_eq(packet.size(), NetCodec.SNAPSHOT_CLIENT_HEADER_SIZE + segment.size(), "full packet size")
assert_eq(packet.size(), 169, "matches multiplayer-todo.md §2.4's 169 B payload figure for 7 bodies")
assert_eq(packet.size(), 169, "matches multiplayer-next.md §2.4's 169 B payload figure for 7 bodies")
var decoded := NetCodec.unpack_snapshot(packet)
assert_eq(decoded["last_input_seq"], 555, "last_input_seq")
+1 -1
View File
@@ -8,7 +8,7 @@ extends Node
# get_tree().current_scene, and calling it from a node that ISN'T an
# ancestor-chain match for current_scene (as an earlier draft of this test
# did, by add_child()-ing lobby.tscn under this driver) hung completely
# on disconnect — see multiplayer-todo.md §9 gotcha 27.
# on disconnect — see multiplayer-next.md §9 gotcha 27.
#
# The host role loading lobby.tscn is deliberate, not an oversight: a
# *dedicated* server (server_boot.tscn) never loads it, but a self-hosting
+1 -1
View File
@@ -97,7 +97,7 @@ func _on_player_joined(peer_id: int, player_name: String) -> void:
_finish(true, "host saw player_joined (peer_id=%d, name=%s)" % [peer_id, player_name])
# Adversarial-review regression (multiplayer-todo.md §9): MatchNet.roster
# Adversarial-review regression (multiplayer-next.md §9): MatchNet.roster
# used to have no path that cleared it when a HOST itself called
# NetworkManager.shutdown() — only the client-side disconnect signal did.
# Host -> client joins -> host leaves (shutdown) -> host again used to
+2 -2
View File
@@ -1,7 +1,7 @@
extends Node
# One-off GPU frame-time profiling harness for task 0.15b's real-hardware
# follow-up (multiplayer-todo.md §5.5.1) — the automated Mac passes gave
# follow-up (multiplayer-next.md §5.5.1) — the automated Mac passes gave
# inconsistent, sometimes implausible numbers (stale-process contention,
# and Apple Silicon's tile-based GPU architecture is a poor stand-in for the
# target reference hardware). Run this directly on a machine with a real
@@ -47,7 +47,7 @@ func _ready() -> void:
var match_scene := load("res://scenes/match.tscn") as PackedScene
_match = match_scene.instantiate()
# 3v3 = 6 ships, matching the scenario multiplayer-todo.md §5.5 measures.
# 3v3 = 6 ships, matching the scenario multiplayer-next.md §5.5 measures.
_match.team_size = 3
# Direct-scene-run fallback path (see match_mode.gd:_make_opponent_controller)
# — gives every AI ship a real trained policy so thruster VFX/movement
+2 -2
View File
@@ -20,10 +20,10 @@ The largest gap between this and a AAA-feeling product is presentation, not code
## Multiplayer (long term)
The concise current checklist is **[`multiplayer-next.md`](multiplayer-next.md)**. Historical architecture decisions, implementation evidence, and completed-task detail stay in **[`multiplayer-todo.md`](multiplayer-todo.md)**. Server-authoritative multiplayer, prediction, ENet dedicated hosting, and the Phase 6 exported-server Docker/CI verification are implemented; the remaining gates are captured in the current checklist.
The single tracking document is **[`multiplayer-next.md`](multiplayer-next.md)** architecture decisions, implementation evidence, and the current checklist all in one place. Server-authoritative multiplayer, prediction, ENet dedicated hosting, and the Phase 6 exported-server Docker/CI verification are implemented; the remaining gates are captured there.
Phase 7 begins with optional GodotSteam bootstrap and a transport boundary; direct-IP ENet remains fully supported. It also carries the **graphics/performance work** — the project has never been profiled, and `video_settings.gd` exposes only AA, glow and brightness while SDFGI, SSIL, SSAO and five shadow-casting lights are on by default and unreachable (see §5.5 there).
**Tasks 0.10.15, 0.180.25, 0.27, 0.29 are done** (see the Phase 0 table in `multiplayer-todo.md` for what each one actually changed — several deviated from the original plan for concrete GDScript/Godot reasons recorded inline). Remaining, all blocked on **0.15b (profile, on reference hardware, in the live editor — not done)**: 0.16 (camera to `_process`), 0.17/0.17b/0.17c/0.17d (graphics presets, vsync, resolution scaling), **0.26 (bake the arena GI to retire SDFGI — the largest frame-time win available, costs no image quality since the arena is fully static)**, and 0.28 (physics separate-thread prototype, flagged as the riskiest task in the phase). These need a human at the editor with real hardware to profile and eyeball, not further code changes.
**Tasks 0.10.15, 0.180.25, 0.27, 0.29 are done** (see the Phase 0 table in `multiplayer-next.md` for what each one actually changed — several deviated from the original plan for concrete GDScript/Godot reasons recorded inline). Remaining, all blocked on **0.15b (profile, on reference hardware, in the live editor — not done)**: 0.16 (camera to `_process`), 0.17/0.17b/0.17c/0.17d (graphics presets, vsync, resolution scaling), **0.26 (bake the arena GI to retire SDFGI — the largest frame-time win available, costs no image quality since the arena is fully static)**, and 0.28 (physics separate-thread prototype, flagged as the riskiest task in the phase). These need a human at the editor with real hardware to profile and eyeball, not further code changes.
- [ ] Possible v0.2 split-screen: spawn one `ship_camera_rig` + viewport per local player (camera is already outside the ship scene to allow this). Unrelated to online play.
+3 -3
View File
@@ -1,9 +1,9 @@
# Matchmaking — casual and ranked queues
Architecture and locked product policy for Phase 8. This is a **1.0 launch
blocker**. The numbered, independently implementable tasks and their
acceptance criteria live in [`multiplayer-todo.md`](../multiplayer-todo.md);
the short live checklist is [`multiplayer-next.md`](../multiplayer-next.md).
blocker**. The numbered, independently implementable tasks, their acceptance
criteria, and current progress all live in
[`multiplayer-next.md`](../multiplayer-next.md).
Nothing in Phase 8 is implemented yet. This document records the decisions
those tasks assume so an implementer does not have to redesign the system
+4 -4
View File
@@ -56,7 +56,7 @@ snapshot/restore API. That fact is why the multiplayer architecture is
server-authoritative with client-side prediction of only the local ship,
rather than rollback/resimulation netcode — rollback would require
deterministic replay, which no physics engine choice here provides
(`multiplayer-todo.md` §1, decision 1).
(`multiplayer-next.md` §1, decision 1).
## Multiplayer transport: Godot's built-in `MultiplayerAPI` over ENet
@@ -71,7 +71,7 @@ Design choices layered on top of the built-in peer, and why:
- **`ENetMultiplayerPeer.server_relay` is forced to `false`.** It defaults
to `true`, which lets any client `rpc()` any other client *through the
server* — incompatible with a server-authoritative model. Called out in
`multiplayer-todo.md` §2.1 as "the single highest-value one-line security
`multiplayer-next.md` §2.1 as "the single highest-value one-line security
change in the document."
- **Manual multiplayer polling**, not Godot's automatic idle-frame poll.
`NetworkManager` calls `set_multiplayer_poll_enabled(false)` because the
@@ -84,7 +84,7 @@ Design choices layered on top of the built-in peer, and why:
- **A custom binary wire format** (`net_codec.gd`) rather than raw RPC
argument marshalling, for compact, quantised input/snapshot packets sent
at high frequency — no stated alternative was considered in the docs, but
the packet-size/channel-intent design in `multiplayer-todo.md` §2 is
the packet-size/channel-intent design in `multiplayer-next.md` §2 is
extensive and deliberate.
## Optional multiplayer transport: Steam (GodotSteam)
@@ -96,7 +96,7 @@ Relay), from a custom GodotSteam-patched Godot build (not stock Godot —
use ENet only, and a build without the `steam` feature is fully functional
without it.
**Why it's optional and why raw ENet remains primary:** `multiplayer-todo.md`
**Why it's optional and why raw ENet remains primary:** `multiplayer-next.md`
states plainly that "Docker/VPS is the primary v1 deployment path. Raw ENet
self-hosting needs port forwarding, and SDR is Phase 7 — so [the ENet
phases] ship something that works on LAN or a VPS and nowhere else." Steam/SDR
+1370 -228
View File
File diff suppressed because one or more lines are too long
-1387
View File
File diff suppressed because one or more lines are too long