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: 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-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.
- `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.
- `TRAINING.md` — the full RL workflow (training, curriculum generations, export, eval, difficulty tiers). - `TRAINING.md` — the full RL workflow (training, curriculum generations, export, eval, difficulty tiers).
- `SERVER.md` — dedicated-server build, config, systemd deploy, sizing. - `SERVER.md` — dedicated-server build, config, systemd deploy, sizing.
- `STEAM.md` — optional GodotSteam custom-build setup and the transport contract. - `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/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. | | `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. **`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 class_name InputJitterBuffer
extends RefCounted 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 # Deliberately a standalone RefCounted with no scene/RPC dependency — same
# reason net_codec.gd and net_interpolator.gd are pure classes — so task # 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 # 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. # class's, since only the caller knows the current server tick.
const RING_SIZE := 32 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 # tick-rate-derived constant, so left as a literal rather than pulling in
# SimConstants for one number. # SimConstants for one number.
const STARVE_ZERO_TICKS := 30 const STARVE_ZERO_TICKS := 30
+1 -1
View File
@@ -1,7 +1,7 @@
class_name InputLeadController class_name InputLeadController
extends RefCounted 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 # Standalone RefCounted, same reason as input_jitter_buffer.gd — scene-free
# so it's directly unit-testable against scripted depth traces. # 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 # of something else: change_scene_to_file() operates on
# get_tree().current_scene, and _on_disconnected_from_server()/_leave() # get_tree().current_scene, and _on_disconnected_from_server()/_leave()
# below call it themselves, which hangs if this scene isn't actually the # 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). # gotcha 27 — found the hard way while building tests/lobby_smoke.gd).
@onready var _status_label: Label = %StatusLabel @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") 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 # This is deliberately independent of NetworkedMatch and the scene tree so
# sequence/ring behaviour can be tested from scripted traces. Each entry is # 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 # tagged with its full sequence number: an old value in a wrapped slot is
+1 -1
View File
@@ -1,7 +1,7 @@
extends Node extends Node
# Autoload (project.godot [autoload] MatchNet). Handshake + roster layer on # 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 # hello/welcome, strict protocol_version and physics_ticks_per_second
# gating, player_joined/player_left, and — since lobby.tscn (task 1.5) needs # gating, player_joined/player_left, and — since lobby.tscn (task 1.5) needs
# somewhere durable to keep it across the lobby→match scene transition — # 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. # §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) 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- # lives here rather than in NetworkedMatch: framing/rate abuse is a protocol-
# level concern independent of any particular match's roster/slot state, and # level concern independent of any particular match's roster/slot state, and
# this autoload already owns the RPC that receives the raw bytes. # this autoload already owns the RPC that receives the raw bytes.
+1 -1
View File
@@ -1,6 +1,6 @@
class_name MatchState 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 # Pure data + a transition table, deliberately with no scene, RPC or
# NetworkedMatch dependency — same reason net_codec.gd and # NetworkedMatch dependency — same reason net_codec.gd and
+1 -1
View File
@@ -1,6 +1,6 @@
extends RefCounted 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 # Deliberately not Ship/Ball themselves, and deliberately not a scene-tree
# node — NetCodec's pack/unpack must stay callable from pure-function tests # 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 # 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 class_name NetCodec
# Wire-format constants, quantisers, and pack/unpack for the two hot-path # 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 # no autoload state — so they're testable head-on by tests/test_runner.tscn
# without a live connection. # without a live connection.
# #
@@ -48,7 +48,7 @@ const BODY_FLAG_STALLED := 1 << 5
const BODY_FLAG_QUAT_W_SIGN := 1 << 6 const BODY_FLAG_QUAT_W_SIGN := 1 << 6
# --- Quantisation ranges (§2.4 — derived from arena/gameplay constants, not # --- 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) --- # constants these are sized against) ---
const POS_RANGE := 64.0 # metres, ± const POS_RANGE := 64.0 # metres, ±
const VEL_RANGE := 64.0 # m/s, ± 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 # Buffers recent snapshot samples for ONE remote body and produces
# interpolated states at any requested (possibly fractional) server tick — # 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 # entities"): once at the present-time estimate for the collider, once
# further back at present-minus-INTERP_DELAY for $Visual. # further back at present-minus-INTERP_DELAY for $Visual.
# #
+1 -1
View File
@@ -1,6 +1,6 @@
extends RefCounted 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 # NetworkedMatch so the decision table is pure-testable; the imperative half
# only writes Ship's existing Jolt-safe queued correction hooks. # 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 return
# process_always = true: a simulated wire delay must keep counting down # process_always = true: a simulated wire delay must keep counting down
# even if the local SceneTree pauses (match_mode.gd's goal-pause does # 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 # 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 # 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 # 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 # Autoload (project.godot [autoload] NetworkManager). Owns transport-neutral
# hosting, joining, shutdown, and connection-state signals. Lives # hosting, joining, shutdown, and connection-state signals. Lives
# at a fixed autoload path so RPC NodePaths never depend on which scene is # 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` # 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 # 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 # 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 # that off; every caller that sends or expects to receive on a tight cadence
# must now call NetworkManager.poll() itself. The intended placement per # 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; # 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 # 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). # 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 # Performance monitors; never touches rendering or gameplay state. Exists so
# 0.17/0.17b's graphics presets and resolution scaling are self-diagnosing — # 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 # 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 # ~2s of history at 60 fps; enough to make p50/p99 meaningful without the
# history itself being a rate-dependent quantity. # history itself being a rate-dependent quantity.
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ReplayLog class_name ReplayLog
extends RefCounted 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 # The highest-value debuggability investment in Phase 5, and cheap precisely
# because the packets are ALREADY flat bytes: this stores them verbatim rather # because the packets are ALREADY flat bytes: this stores them verbatim rather
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerConfig class_name ServerConfig
extends RefCounted 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`. # declaration of every server flag, one parser, one `--help`.
# #
# Standalone RefCounted with no scene or RPC dependency — same reason as # Standalone RefCounted with no scene or RPC dependency — same reason as
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerLog class_name ServerLog
extends RefCounted 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 # Extracted from server_boot.gd's private `_log`, which could only ever see
# what the boot scene itself observed: connects, disconnects, roster changes # what the boot scene itself observed: connects, disconnects, roster changes
+1 -1
View File
@@ -1,7 +1,7 @@
class_name ServerMatchLoop class_name ServerMatchLoop
extends Node 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 # 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: # 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 _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 # Both stay zero until Phase 4 wires a reconciliation pass in, so the guarded
# hook in _integrate_forces below is a no-op today. # hook in _integrate_forces below is a no-op today.
# Velocity delta from a soft correction, consumed once then cleared — # 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, # All rendered geometry (hull, canopy, engine cores/flames/lights, Nose,
# TailFin) parents under this instead of the RigidBody3D directly, so a # TailFin) parents under this instead of the RigidBody3D directly, so a
# future prediction correction (task 0.14) can offset the visual without # 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. # and the controller child correctly stay on the body itself.
@onready var visual: Node3D = $Visual @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, # constant derived from "60 Hz" (Ship._tick_scaled's decay reference,
# reaction_ticks' export range, TrainingMode.TICKS_PER_SIM_SECOND) reads this # reaction_ticks' export range, TrainingMode.TICKS_PER_SIM_SECOND) reads this
# instead of restating the literal, so changing it changes every derived # 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 # simulation needs to be a config change plus a retrain, not a protocol
# rewrite hunting down bare 60s. # 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 # 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 # 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 # 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 # understanding before stretch mode is touched, not blindly carrying into a
# resolution-dependent change. # resolution-dependent change.
# #
@@ -49,7 +49,7 @@ const SETTINGS_PATH := "user://settings.cfg"
# preset -> bundle applied to the individual fields below. CUSTOM has no # preset -> bundle applied to the individual fields below. CUSTOM has no
# bundle: selecting it just stops future preset changes from overwriting # bundle: selecting it just stops future preset changes from overwriting
# whatever the individual fields currently hold. Task 0.15b's measured # 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 # against each other, so each rung is "meaningfully fewer full-screen passes
# than the one above it" rather than a precisely tuned ladder. # than the one above it" rather than a precisely tuned ladder.
const PRESET_BUNDLES := { const PRESET_BUNDLES := {
@@ -78,7 +78,7 @@ var shadows_enabled: bool = true
var glow_enabled: bool = true var glow_enabled: bool = true
# FXAA alone, not MSAA_FXAA: 4x MSAA *and* FXAA stacked is redundant blur for # 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 aa_mode: AAMode = AAMode.FXAA
var glow_scale: float = 1.0 var glow_scale: float = 1.0
var brightness: 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 # 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 # 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 # 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: func apply_to_environment(env: Environment) -> void:
if env == null: if env == null:
return 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) 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(), 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) var decoded := NetCodec.unpack_snapshot(packet)
assert_eq(decoded["last_input_seq"], 555, "last_input_seq") 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 # 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 # 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 # 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 # The host role loading lobby.tscn is deliberate, not an oversight: a
# *dedicated* server (server_boot.tscn) never loads it, but a self-hosting # *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]) _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 # used to have no path that cleared it when a HOST itself called
# NetworkManager.shutdown() — only the client-side disconnect signal did. # NetworkManager.shutdown() — only the client-side disconnect signal did.
# Host -> client joins -> host leaves (shutdown) -> host again used to # Host -> client joins -> host leaves (shutdown) -> host again used to
+2 -2
View File
@@ -1,7 +1,7 @@
extends Node extends Node
# One-off GPU frame-time profiling harness for task 0.15b's real-hardware # 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, # inconsistent, sometimes implausible numbers (stale-process contention,
# and Apple Silicon's tile-based GPU architecture is a poor stand-in for the # 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 # 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 var match_scene := load("res://scenes/match.tscn") as PackedScene
_match = match_scene.instantiate() _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 _match.team_size = 3
# Direct-scene-run fallback path (see match_mode.gd:_make_opponent_controller) # Direct-scene-run fallback path (see match_mode.gd:_make_opponent_controller)
# — gives every AI ship a real trained policy so thruster VFX/movement # — 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) ## 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). 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. - [ ] 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 # Matchmaking — casual and ranked queues
Architecture and locked product policy for Phase 8. This is a **1.0 launch Architecture and locked product policy for Phase 8. This is a **1.0 launch
blocker**. The numbered, independently implementable tasks and their blocker**. The numbered, independently implementable tasks, their acceptance
acceptance criteria live in [`multiplayer-todo.md`](../multiplayer-todo.md); criteria, and current progress all live in
the short live checklist is [`multiplayer-next.md`](../multiplayer-next.md). [`multiplayer-next.md`](../multiplayer-next.md).
Nothing in Phase 8 is implemented yet. This document records the decisions 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 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, server-authoritative with client-side prediction of only the local ship,
rather than rollback/resimulation netcode — rollback would require rather than rollback/resimulation netcode — rollback would require
deterministic replay, which no physics engine choice here provides 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 ## 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 - **`ENetMultiplayerPeer.server_relay` is forced to `false`.** It defaults
to `true`, which lets any client `rpc()` any other client *through the to `true`, which lets any client `rpc()` any other client *through the
server* — incompatible with a server-authoritative model. Called out in 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." change in the document."
- **Manual multiplayer polling**, not Godot's automatic idle-frame poll. - **Manual multiplayer polling**, not Godot's automatic idle-frame poll.
`NetworkManager` calls `set_multiplayer_poll_enabled(false)` because the `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 - **A custom binary wire format** (`net_codec.gd`) rather than raw RPC
argument marshalling, for compact, quantised input/snapshot packets sent argument marshalling, for compact, quantised input/snapshot packets sent
at high frequency — no stated alternative was considered in the docs, but 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. extensive and deliberate.
## Optional multiplayer transport: Steam (GodotSteam) ## 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 use ENet only, and a build without the `steam` feature is fully functional
without it. 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 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 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 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