2026-06-08 16:55:24 +08:00

10 KiB

name, description
name description
th1-network-sync TH1 project-specific network synchronization guide for Unity C# multiplayer, Steam P2P, GameNetSender/GameNetReceiver, SteamLobbyManager, SimpleP2P, lobby MapConfig/MemberCiv player slot/team/AI/ready-state sync before game start, MapData/NetData recovery and diagnostics, action sync, host start/resume, ForceUpdate, heartbeat, ordered delivery, large message chunking, send-failure handling, and lobby UI rollback. Use whenever Codex works on TH1 networking, multiplayer rooms, member civ/ready/team/AI slot config, multiplayer saves, deterministic sync, P2P queueing, MapData broadcasts or MapDataDebug diff tools, ActionConfirm/ActionExecute, reconnect, Timer-driven network callbacks, or any bug that may affect multiplayer reliability or ordering.

TH1 Network Sync

Core Rule

Treat multiplayer changes as state-machine changes, not only message sends. Before editing, trace the full path:

GameNetSender -> SteamLobbyManager -> SimpleP2P -> GameNetReceiver -> Main/ActionLogic/MapData.

Do not let one peer advance local game state unless the matching network send/receive contract succeeded.

First Files To Read

  • Unity/Assets/Scripts/TH1_Logic/Steam/SimpleP2P.cs
  • Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs
  • Unity/Assets/Scripts/TH1_Logic/Steam/GameNetSender.cs
  • Unity/Assets/Scripts/TH1_Logic/Steam/GameNetReceiver.cs
  • Unity/Assets/Scripts/TH1_Data/NetData.cs
  • Unity/Assets/Scripts/TH1_Data/MapData.cs
  • Unity/Assets/Scripts/TH1_Logic/Core/Main.cs
  • Unity/Assets/Scripts/TH1_Logic/Action/ActionLogic.cs
  • Unity/Assets/Scripts/TH1_Instance/Timer.cs
  • Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs when changing or interpreting network stress tests or the Tools/Steam MapData一致性诊断 editor window

For the current network contract summary, read references/network-contract.md. For the one-click Steam P2P stress tool, report fields, and current healthy baseline, read references/network-stress.md.

Workflow

  1. Classify the message.

    • Critical: GameStart, ForceUpdate, ActionConfirm, ActionExecute, full MapData, reconnect/restore messages.
    • Health/status: heartbeat, map confirm, lobby state/config, member ready state, chat.
    • Critical messages must return bool or otherwise expose failure to the caller before local state advances.
  2. Preserve ordering.

    • Keep game messages on SimpleP2P per-peer outgoing queues.
    • Do not bypass ordered envelopes for normal game messages.
    • Do not send direct raw Steam messages for gameplay sync unless you also preserve FIFO and failure semantics.
  3. Preserve all-or-nothing broadcast for critical messages.

    • Broadcast through SteamLobbyManager.BroadcastMessage.
    • Preflight all lobby targets before enqueueing any target.
    • If any target cannot queue, return failure and avoid local progression.
    • Never accept "some peers got the critical message" as a valid state.
  4. Gate local state on send success.

    • Host GameStart must succeed before SaveMapData, RefreshTurn, room UI close, or final success return.
    • Owner ActionExecute broadcast must succeed before owner local execute.
    • Client ActionConfirm must succeed before client local execute; TurnEnd may send and then stop local execution.
    • Heartbeat send timestamps should only update after the send was accepted.
  5. Validate MapData before sending or applying.

    • Reject null maps, DeserializedMissingCriticalData, wrong NetMode, and missing core maps.
    • Call Net.RefreshPlayerNet(mapData) and respect its bool result.
    • Ensure every current lobby member maps to a valid PlayerId.
    • Do not repair missing critical data silently during deserialization.
  6. Keep lobby MapConfig host-authoritative.

    • Pre-game room settings, MemberCiv, player slot/team/AI flags, and ready state live in Main.Instance.MapConfig.
    • MapConfig.MultiCivs is now the full player-slot list and should be sized to PlayerCount; do not treat it as only current lobby members.
    • Each MemberCiv slot uses Index as the stable player position. MemberId != 0 means a real member is bound, MemberId == 0 && IsAI means an AI slot, MemberId == 0 && !IsAI && IsHostControlled means a host/local-controlled real player slot, and TeamId == 0 means no team.
    • Host-controlled slots are real players, not AI and not lobby members. Keep IsReady=false, do not add them to Net.Players, skip them when assigning/moving real lobby members, and allow them in AreAllLobbyMembersReady().
    • Net.RefreshPlayerNet(mapData) maps only current lobby MemberId values to PlayerId; host-controlled slots must not require a Steam member mapping or duplicate the host's mapping.
    • Local control should go through MapData.CanLocalControlPlayer(...): in singleplayer all real local slots are controllable; in multiplayer the member's mapped slot is controllable, and only the lobby owner may also control IsHostControlled slots. When switching turn identity for UI/input, restore the default local member slot on non-local turns.
    • Call MapConfig.EnsurePlayerSlots(NetMode.Multi) before reading or mutating lobby slots, especially after changing PlayerCount or receiving host config.
    • Clients may optimistically update their own MemberCiv only after ChangeCiv send succeeds, then still accept host UpdateLobbyData as authority.
    • Clients entering a room should request host lobby data until current lobby members match MapConfig.MultiCivs.
    • Host must refresh lobby members before sending lobby config; new guests default not ready, and the owner is always ready.
    • Changing civ, team, AI slot ownership, slot assignment, or host room settings should clear guest ready state.
    • UI room-row reconciliation must count host-controlled slots as occupied seats, not open seats, and must not auto-convert them between open and AI.
    • Host start/resume must require AreAllLobbyMembersReady().
    • Net.RefreshPlayerNet(mapData) maps real lobby MemberId values to the slot-created PlayerId; AI and host-controlled slots must not require a lobby member mapping.
    • TeamId drives in-game teammate diplomacy, so host and clients must agree on the full slot list before GameStart.
  7. Roll back failed start/resume.

    • Snapshot MapData, InputLogic, MapInteractionLogic, and MapGeneratorLogic before host start/resume mutation.
    • On failure or exception, restore the snapshot, dispose/reinitialize render state as appropriate, cancel pending start timers, and keep lobby UI open.
    • UI code must only invoke room-close/start callbacks after the start method returns true.
  8. Keep receiver failure atomic.

    • Deserialize and dispatch inside try/catch.
    • If incoming GameStart or ForceUpdate validation fails, do not hide room UI and do not leave the game in ForceUpdating.
    • Restore previous game state if NetResumeMatch fails.
  9. Preserve the stress-test path.

    • NetworkStressMessage is a diagnostics message and must not mutate gameplay state.
    • GameNetReceiver should ignore P2PMsgType.NetworkStress; the editor tool listens through SimpleP2P.OnMessageReceivedEvent.
    • Stress probes must still use Lobby.BroadcastMessage / SendMessageToPeer so they cover the real ordered queue and large-message chunking path.
    • Keep per-run RunId isolation so stale packets or ACKs from a previous test cannot pollute a new report.
    • The host should export after collecting client reports or after the configured report wait timeout; clients should retry report sends briefly after test completion.
  10. Use MapDataDebugMessage for pre-reconnect divergence captures.

  • P2PMsgType.MapDataDebug carries current MapData only for diagnostics: member-to-host or host-broadcast.
  • The receiver compares the incoming map against local Main.MapData and logs [MapDataDebug]; it must not call NetResumeMatch, change GameState, request ForceUpdate, or mutate gameplay state.
  • The editor entry is Tools/Steam MapData一致性诊断; use it when normal ForceUpdate diff logs are too late because the host has already advanced.
  • Keep the send path on GameNetSender / lobby queues so large-map diagnostics still cover ordered P2P chunking.

Checks Before Finishing

Run:

dotnet build Unity/Assembly-CSharp.csproj --no-restore

If editor tooling changed, also run:

dotnet build Unity/Assembly-CSharp-Editor.csproj --no-restore

For network-heavy changes, inspect these risks explicitly:

  • Could a critical broadcast partially enqueue?
  • Could a caller ignore a failed send and still mutate game state?
  • Could a client treat optimistic MemberCiv or ready state as authoritative before host UpdateLobbyData?
  • Could the host start/resume while any guest is not ready or current lobby members do not match MapConfig.MultiCivs?
  • Could an empty non-AI slot (MemberId == 0 && !IsAI && !IsHostControlled) start the game accidentally?
  • Could a host-controlled slot be counted as an open UI seat, converted to AI/open by reconciliation, or inserted into Net.Players?
  • Could code reintroduce the old assumption that MultiCivs.Count == current lobby member count?
  • Could MemberCiv.Index, PlayerId, or TeamId diverge between host and clients before GameStart?
  • Could MapData deserialize with missing core fields and still be used?
  • Could MapDataDebugMessage accidentally trigger reconnect, ForceUpdate, or any map mutation?
  • Could a timer callback fire after the target UI/object state is gone?
  • Could a retry loop or ordered gap wait forever?
  • If stress tooling or queue throughput changed, does the one-click two-machine report meet the baseline in references/network-stress.md?

What Not To Do

  • Do not add a new direct send path around SimpleP2P queues for gameplay sync.
  • Do not silently swallow SendMessageToPeer or BroadcastMessage failure.
  • Do not call GameNetReceiver from a partial large-message chunk.
  • Do not close the multiplayer room UI before host start returns success.
  • Do not add separate pre-game ready/config state outside MapConfig unless you also define host-authoritative reconciliation.
  • Do not shrink MapConfig.MultiCivs to only real lobby members; it represents every player slot in the match.
  • Do not derive player position from lobby member order after slots exist; use MemberCiv.Index.
  • Do not start the host game from client-local optimistic ready state.
  • Do not call GC.Collect() in match entry paths as a networking fix.
  • Do not use MapDataDebugMessage as a repair/sync mechanism; it is logging-only diagnostics.