# Unity Multiplayer C# 事件参考

目标边界下，`cn.indiegp.sdk.unity.multiplayer` 只公开 Game data-plane 事件：
`ConnectionChanged`、`ConnectionInterrupted`、`ConnectionRecovered`、
`MessageReceived`、`ErrorOccurred` 和 `IGPNetwork.DataReceived`。KCP、TCP
和 UDP 底层客户端不是公开 API。

当前源码仍额外公开 `RoomChanged`，并混入 Hosted 来源。该事件属于已确认的
边界越界，迁移到 Lobby 后应从 Runtime 删除；本页保留其现状说明仅用于审计。

所有事件均为代码订阅的 `System.Action`，不显示在 Inspector 中。Runtime 会先完成状态更新、
协议分类和必要的分片重组，再调用公开事件。新订阅者不会收到历史事件，线程模型和普通
`?.Invoke` 行为保持不变。

## 订阅示例

```csharp
using IGP.Multiplayer;
using IGP.Multiplayer.Network;
using UnityEngine;

public sealed class MultiplayerEvents : MonoBehaviour
{
    [SerializeField] private IGPMultiplayerRuntime runtime = null!;

    private void OnEnable()
    {
        runtime.ConnectionChanged += OnConnectionChanged;
        runtime.ConnectionInterrupted += OnConnectionInterrupted;
        runtime.ConnectionRecovered += OnConnectionRecovered;
        runtime.MessageReceived += OnMessageReceived;
        runtime.ErrorOccurred += OnError;
    }

    private void OnDisable()
    {
        runtime.ConnectionChanged -= OnConnectionChanged;
        runtime.ConnectionInterrupted -= OnConnectionInterrupted;
        runtime.ConnectionRecovered -= OnConnectionRecovered;
        runtime.MessageReceived -= OnMessageReceived;
        runtime.ErrorOccurred -= OnError;
    }

    private static void OnConnectionChanged(IGPMultiplayerConnectionChangedEvent e) { }
    private static void OnConnectionInterrupted(IGPMultiplayerConnectionChangedEvent e) { }
    private static void OnConnectionRecovered(IGPMultiplayerConnectionChangedEvent e) { }
    private static void OnMessageReceived(IGPMessageReceivedEvent e) { }
    private static void OnError(IGPMultiplayerErrorEvent e) { }
}
```

`Network` 在 Multiplayer 初始化时创建。需要二进制数据时，在
`await IGPRuntimeManager.InitializeAsync()` 成功返回后订阅 `runtime.Network.DataReceived`，并在组件停用时取消。

## Runtime 事件

### `ConnectionChanged`

```csharp
event Action<IGPMultiplayerConnectionChangedEvent> ConnectionChanged;
```

含义：RoomNode reliable Game 通道整体连接状态发生变化。

目标触发时机：调用方提交 descriptor 后，Runtime 在 `Idle`、`Connecting`、
`Ready`、`Failed` 之间切换并更新内部状态后触发。KCP 或 TCP 认证完成进入 `Ready`；当前 reliable 通道断开
或终止故障进入 `Failed`。UDP 是可选通道，单独连接或非终止故障不会改变整体 Ready 状态。

参数：`PreviousState`、`State`、`IsReady` 和 `Reason`。`IsReady` 仅在 `State == Ready` 时为真。

真实来源：显式 descriptor 生命周期和 RoomNode KCP/TCP 连接/故障。状态未变化时不触发，订阅时不重放。

### `ConnectionInterrupted`

```csharp
event Action<IGPMultiplayerConnectionChangedEvent> ConnectionInterrupted;
```

含义：一个已经进入 `Ready` 的 reliable Game 通道被确认断开或发生终止故障。
事件只在一次中断的首次确认时触发；初次连接失败不会触发此事件。参数中的
`PreviousState` 为 `Ready`、`State` 为 `Failed`，`Reason` 为断开或故障原因。
该事件只通知上层，Multiplayer 不重建 Mirror peer、场景或业务状态。

### `ConnectionRecovered`

```csharp
event Action<IGPMultiplayerConnectionChangedEvent> ConnectionRecovered;
```

含义：调用方显式调用 `ReconnectDataPlaneAsync`（或等价的显式创建入口）后，
新的 descriptor 请求和 reliable 握手已经完成。事件只在断线后的显式重连成功后
触发一次；初次连接成功不会触发。参数中的 `State` 为 `Ready`，`IsReady` 为真，
`Reason` 为 `explicit-reconnect`。

该事件仅表示 Game data plane 已重新可用。Arena 不迁移旧连接的发送队列，也不回放
业务消息；上层仍需自行重新建立 Mirror peer 并同步权威游戏状态。

### Legacy `RoomChanged` (boundary violation)

```csharp
event Action<IGPRoomChangedEvent> RoomChanged;
```

此事件及其房间投影应迁移到 Lobby。Runtime 可以保留 Game peer/slot 和
Scene Gate 等数据面状态，但不得把它们合成为 Lobby 房间快照。

精确触发时机：

- 收到首个有效 Hosted room snapshot 后触发 `Joined`。
- 同一房间 snapshot 与当前状态存在实际差异时触发 `Updated`。
- `scene_gate_set/get` 实际改变当前值时触发 `Updated | SceneGate`。
- reliable 队伍结构化消息实际改变队伍/玩家队伍状态时触发 `Updated`。
- 房间切换时先对旧房间触发 `Left`，再对新房间触发 `Joined`。
- 主动断开或 Hosted Session 意外脱离时，对已存在的房间触发一次 `Left`。

参数：

- `Kind`：`Joined`、`Updated` 或 `Left`。
- `PreviousRoom`：变更前的独立快照；首次加入时为空。
- `Room`：变更后的独立快照；离开时为空。
- `CurrentPlayer`：当前玩家快照；离开时为空。
- `Changes`：`Players`、`Host`、`Status`、`Map`、`SceneGate`、`Teams`、`Metadata` 的 flags。
- `Reason`：`hosted-snapshot`、`room-switched`、`scene-gate-*`、`realtime-team` 或 `disconnect` 等来源说明。

真实来源：Desktop/Hosted Session 转交的 Arena room snapshot，以及 Arena reliable Scene Gate/Team 消息。
旧 `roomEvent` 只保留兼容接收，房间事实以 snapshot 为准，因此不会与 snapshot 重复通知。

去重：相同 snapshot、相同 Scene Gate 值和无实际队伍变化不触发。订阅时不重放。

### `MessageReceived`

```csharp
event Action<IGPMessageReceivedEvent> MessageReceived;
```

含义：收到一条完整的 Arena reliable 结构化业务消息。

会触发的消息：

- 游戏自定义消息。
- `state_set`、`state_get`、`state_reset` 及其结构化响应。
- `rpc_register`、`rpc_unregister`、`rpc_call`、`rpc_response`。

不会触发的消息：

- `p2p_data`，它只进入 `IGPNetwork.DataReceived`。
- UDP 数据；UDP 当前只承载 `p2p_data`。
- ping、pong、token、room command 等控制消息。
- 目标 Runtime 不接收 Team 或 room lifecycle 消息。当前遗留实现仍会消费部分
  Scene Gate/Team 消息并映射到 `RoomChanged`，这属于待删除的边界越界。
- Arena `error`，它映射到 `ErrorOccurred`。

参数：`MessageType`、`RoomId`、`SenderPlayerId`、`TargetPlayerId`、转换后的 `Content` 和 `Timestamp`。
该类型没有 `Transport` 字段；KCP 与 TCP 使用同一套结构化消息语义。

State/RPC content 会转换为对应的 `State*Content`、`RPC*Content` 类型；自定义 content 保持可序列化对象。
每条完整 reliable 业务消息交付一次，订阅时不重放。

### `ErrorOccurred`

```csharp
event Action<IGPMultiplayerErrorEvent> ErrorOccurred;
```

目标含义：RoomNode KCP、TCP、UDP、Game 业务或协议处理失败。

参数：

- `Code`、`Message`：结构化错误码和说明。
- `Source`：目标只允许 `runtime`、`transport`、`roomnode` 或 `protocol` 等数据面来源。当前 `hosted-session` / `hosted-data-plane` 来源属于待删除越界。
- `Transport`：错误明确属于数据通道时为 `Kcp`/`Tcp`/`Udp`，否则为空。
- `IsTerminal`：该错误是否终止当前必要连接或初始化路径。
- `Details`：原始异常、Arena error content 或底层故障详情（可用时）。

Arena `error` 是非终止业务错误，原始字段保存在 `Details`。reliable 终止故障同时推动一次
`ConnectionChanged(Failed)`；同一故障不再从多个旧错误入口重复报告。订阅时不重放。

TCP 稳定错误码为：`TCP_DATA_PLANE_UNAVAILABLE`、`TCP_DESCRIPTOR_INVALID`、
`TCP_CONNECT_FAILED`、`TCP_HANDSHAKE_TIMEOUT`、`TCP_AUTH_REJECTED`、
`TCP_ENVELOPE_NEGOTIATION_FAILED`、`TCP_FRAME_INVALID`、`TCP_SEND_FAILED` 和
`TCP_RECEIVE_FAILED`。`PreferTcp` 将要使用新 token 回退 KCP 时，TCP attempt 错误的
`IsTerminal` 为 `false`；`TcpOnly` 或后续 KCP 失败才终止本次 data-plane attach。

## Network 事件

### `DataReceived`

```csharp
event Action<IGPDataReceived> DataReceived;
```

含义：收到一份完整、有效，且必要时已完成可靠分片重组的 P2P 二进制数据。

精确触发时机：

- KCP/TCP `p2p_data` 的普通 payload 校验并解码成功后。
- KCP/TCP 可靠分片全部到达并通过重组校验后；单个分片不会触发。
- UDP `p2p_data` envelope 校验并解码成功后。
- RTT、可靠传输控制消息、被 block 的 `message_type` 和畸形 payload 不触发。

参数保留 `remote_peer`、`data`、`message_type`，并增加 `Transport`（`Kcp`、`Tcp` 或 `Udp`）。

同一 `p2p_data` 只触发一次 `DataReceived`，不会同时触发 `MessageReceived`。Mirror 使用包内 backend
消费同一处理结果，不要求游戏同时订阅本事件。订阅时不重放。
