压测工具

This commit is contained in:
wuwenbo 2026-05-15 16:26:17 +08:00
parent ac1b31f33a
commit 6162094162
7 changed files with 1551 additions and 1 deletions

View File

@ -0,0 +1,92 @@
---
name: th1-network-sync
description: TH1 project-specific network synchronization guide for Unity C# multiplayer, Steam P2P, GameNetSender/GameNetReceiver, SteamLobbyManager, SimpleP2P, MapData/NetData recovery, 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 saves, deterministic sync, P2P queueing, MapData broadcasts, 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`
For the current network contract summary, read `references/network-contract.md`.
## Workflow
1. Classify the message.
- Critical: `GameStart`, `ForceUpdate`, `ActionConfirm`, `ActionExecute`, full `MapData`, reconnect/restore messages.
- Health/status: heartbeat, map confirm, lobby 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. 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`.
7. 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.
## Checks Before Finishing
Run:
```powershell
dotnet build Unity/Assembly-CSharp.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 `MapData` deserialize with missing core fields and still be used?
- Could a timer callback fire after the target UI/object state is gone?
- Could a retry loop or ordered gap wait forever?
## 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 call `GC.Collect()` in match entry paths as a networking fix.

View File

@ -0,0 +1,4 @@
interface:
display_name: "TH1 Network Sync"
short_description: "TH1 multiplayer sync and Steam P2P reliability rules"
default_prompt: "Use the TH1 Network Sync skill for TH1 multiplayer, P2P, GameNet, save resume, reconnect, or deterministic sync work."

View File

@ -0,0 +1,67 @@
# TH1 Network Contract
This reference summarizes the multiplayer contract after the May 2026 pre-release network audit.
## SimpleP2P
- All normal game messages use per-peer outgoing queues.
- Per-peer FIFO is required.
- Ordered envelopes are used for game payloads; do not bypass them for gameplay sync.
- Outgoing sequence is committed only after enqueue succeeds.
- Large payloads are chunked after ordered wrapping.
- Incoming large chunks must validate magic, version, message id, chunk index, chunk count, total length, and payload length.
- Large incoming messages and outgoing queues have per-peer and global byte budgets.
- Ordered gaps and large-message receives must timeout and disconnect/clean state rather than wait forever.
- Steam message pointers must be released in `finally`.
## SteamLobbyManager
- `SendMessageToPeer` returns `bool`; precheck failures must call the same failure path used by P2P send failures.
- `BroadcastMessage` returns `bool`.
- Critical broadcast must gather current lobby members, skip self, preflight all targets through `SimpleP2P.CanQueueMessages`, and only then enqueue.
- Missing connection, non-lobby target, invalid data, or queue budget failure must surface through `OnLobbyErrorEvent` / send-failure logging.
## GameNetSender
- Sender methods that gate local state must return `bool`.
- `GameStart` must validate `MapData` and return broadcast success.
- `ActionConfirm` must return send success.
- `ActionExecute` must return broadcast success.
- `ForceUpdate` and full `MapData` sends must validate multiplayer map data before sending.
- Heartbeat send timestamps should only update after send acceptance.
## GameNetReceiver
- Wrap deserialization and dispatch in try/catch.
- Validate incoming `GameStart` and `ForceUpdate` maps before applying.
- `NetStartGame` and `NetResumeMatch` return `bool`; UI should only close/hide after success.
- `ForceUpdate` should restore previous game state if resume fails.
- `MapConfirm` must guard null maps, missing actions, and null action payloads.
## MapData And NetData
- `MapData.DeserializedMissingCriticalData` means the save/network map must not be used.
- `OnAfterMemoryPackDeserialize` may coalesce non-critical containers to avoid callback NREs but must not silently accept missing core data.
- `NetData.RefreshPlayerNet(MapData)` returns `bool`.
- In multiplayer mode, every current lobby member must have a valid, non-duplicate `PlayerId`.
- Host resume/start must fail before assigning global `MapData` if player network mapping is invalid.
## Main And UI
- Host start/resume snapshots `MapData`, `InputLogic`, `MapInteractionLogic`, and `MapGeneratorLogic`.
- Host `GameStart` failure must roll back and must not save, refresh turn, or report success.
- Custom map load must check `mapRecord == null` before `RegenerateMap`.
- `UIOutsideMultiplayView.ShowLoadingAndStartGame` must only invoke `OnStartGame` after the host start/resume method returns `true`.
- Timer callbacks for start announcements should be cancelled during abort paths.
## ActionLogic
- Client `ActionConfirm` failure aborts local action execution.
- Owner `ActionExecute` broadcast failure aborts owner local action execution.
- `TurnEnd` can send confirmation and stop local execution as designed.
## Timer/Event Notes
- Timer callbacks should guard destroyed Unity targets.
- Timer mutation during callback must not remove the wrong task.
- Event publish should isolate listener exceptions so one bad listener does not block others.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2a0b645d31ea4e8aaee2ffadbe47b7cb
timeCreated: 1778849300

View File

@ -22,6 +22,7 @@ namespace TH1_Logic.Steam
{
var message = MemoryPack.MemoryPackSerializer.Deserialize<BaseMessage>(data);
if (message == null) return;
if (message.MessageType == P2PMsgType.NetworkStress) return;
if (message.MessageType == P2PMsgType.String) OnReceivedString((StringMessage)message);
if (message.MessageType == P2PMsgType.GameStart) OnReceivedGameStart((GameStartMessage)message);

View File

@ -57,6 +57,17 @@ namespace TH1_Logic.Steam
ChatMessage = 15,
// 邀请
InviteMessage = 16,
// 网络压测工具消息
NetworkStress = 17,
}
public enum NetworkStressMessageKind : byte
{
Probe = 0,
Ack = 1,
ControlStart = 2,
ControlStop = 3,
Report = 4,
}
@ -77,6 +88,7 @@ namespace TH1_Logic.Steam
[MemoryPackUnion(14, typeof(HeartbeatReplyMessage))]
[MemoryPackUnion(15, typeof(ChatMessage))]
[MemoryPackUnion(16, typeof(InviteMessage))]
[MemoryPackUnion(17, typeof(NetworkStressMessage))]
public abstract partial class BaseMessage
{
public abstract P2PMsgType MessageType { get; }
@ -217,4 +229,37 @@ namespace TH1_Logic.Steam
public override P2PMsgType MessageType => P2PMsgType.InviteMessage;
public LobbyListInfo LobbyInfo { get; set; }
}
}
[MemoryPackable]
public partial class NetworkStressMessage : BaseMessage
{
public override P2PMsgType MessageType => P2PMsgType.NetworkStress;
public NetworkStressMessageKind StressKind { get; set; }
public string SessionId { get; set; }
public string ScenarioName { get; set; }
public ulong SenderMemberId { get; set; }
public ulong TargetMemberId { get; set; }
public long Sequence { get; set; }
public long AckSequence { get; set; }
public long SentUtcTicks { get; set; }
public int PayloadBytes { get; set; }
public int PayloadHash { get; set; }
public bool Reliable { get; set; }
public bool RequiresAck { get; set; }
public float DurationSeconds { get; set; }
public float MessagesPerSecond { get; set; }
public int SmallPayloadBytes { get; set; }
public int LargePayloadBytes { get; set; }
public int LargeEveryMessages { get; set; }
public float DropPercent { get; set; }
public float UnreliablePercent { get; set; }
public int JitterMinMs { get; set; }
public int JitterMaxMs { get; set; }
public int BurstPauseEveryMessages { get; set; }
public int BurstPauseMs { get; set; }
public int RandomSeed { get; set; }
public byte[] Payload { get; set; }
public string Text { get; set; }
}
}