Compare commits
22 Commits
de24868e9b
...
75a713470f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a713470f | ||
|
|
e756c83c4e | ||
|
|
926a33277f | ||
|
|
a0119552ab | ||
|
|
439e0d6b80 | ||
|
|
d0d06f1129 | ||
|
|
a5010f2879 | ||
|
|
afdab4e15b | ||
|
|
dc427049dc | ||
|
|
c3991ae561 | ||
|
|
333b4c2254 | ||
|
|
38ff01a773 | ||
|
|
97351d9eff | ||
|
|
9e38994185 | ||
|
|
2935cb0206 | ||
|
|
031e668c98 | ||
|
|
eeebd28e2e | ||
|
|
38d416fe82 | ||
|
|
ac880e8964 | ||
|
|
40e3700370 | ||
|
|
706e126e73 | ||
|
|
6bc7b558bf |
@@ -1,16 +0,0 @@
|
||||
namespace stick.plugins.kcpable.Helper;
|
||||
|
||||
public class Timer
|
||||
{
|
||||
private ulong _startTimeMs;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_startTimeMs = TimeStore.GetTimeMs();
|
||||
}
|
||||
|
||||
public ulong ElapsedMs()
|
||||
{
|
||||
return TimeStore.GetTimeMs() - _startTimeMs;
|
||||
}
|
||||
}
|
||||
45
Patches/Patch_CheckMessageType_Hello.cs
Normal file
45
Patches/Patch_CheckMessageType_Hello.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 P2PPackageHandler.CheckMessageType,处理 MsgType.Hello (75)。
|
||||
/// 游戏原代码在 switch/case 中对未知 MsgType 会 throw Exception。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_CheckMessageType_Hello
|
||||
{
|
||||
private static Type _p2pHandlerType;
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
_p2pHandlerType = AccessTools.TypeByName("P2PPackageHandler");
|
||||
if (_p2pHandlerType == null) return null;
|
||||
var msgTypeEnum = AccessTools.TypeByName("P2PPackageHandler+MsgType");
|
||||
return AccessTools.Method(_p2pHandlerType, "CheckMessageType",
|
||||
new Type[] { typeof(byte[]), msgTypeEnum, typeof(CSteamID) });
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(object __instance, byte[] data, object type, CSteamID steamIdRemote)
|
||||
{
|
||||
// type 是 P2PPackageHandler.MsgType 枚举,Hello = 75
|
||||
int msgTypeValue = (int)Convert.ChangeType(type, typeof(int));
|
||||
if (msgTypeValue != (int)MsgType.Hello)
|
||||
return true; // 非 Hello,走原逻辑
|
||||
|
||||
// 解析 Hello 包: data = [version, mode]
|
||||
var hello = Hello.FromBytes(data);
|
||||
|
||||
UnityEngine.Debug.Log($"[Kcpable] Hello received from {steamIdRemote}, mode={hello.mode}");
|
||||
|
||||
ConnectionManager.Instance.OnHelloReceived(steamIdRemote, hello);
|
||||
return false; // 跳过原方法,不抛异常
|
||||
}
|
||||
}
|
||||
209
Patches/Patch_LobbyEvents.cs
Normal file
209
Patches/Patch_LobbyEvents.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.OnLobbyEnter(LobbyEnter_t, bool)。
|
||||
/// 进入大厅时调用 ConnectionManager.SetCurrentLobby。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_LobbyEnter
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
// 精确匹配 (LobbyEnter_t, bool) 重载,避免匹配到仅 LobbyEnter_t 的空重载
|
||||
var lobbyEnterType = typeof(LobbyEnter_t);
|
||||
return AccessTools.Method(type, "OnLobbyEnter",
|
||||
new Type[] { lobbyEnterType, typeof(bool) });
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
public static void Postfix(object __instance, object pCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通过反射获取 m_ulSteamIDLobby 字段
|
||||
var field = typeof(LobbyEnter_t).GetField("m_ulSteamIDLobby");
|
||||
if (field == null) return;
|
||||
ulong lobbyIdRaw = (ulong)field.GetValue(pCallback);
|
||||
var lobbyId = new CSteamID(lobbyIdRaw);
|
||||
if (!lobbyId.IsValid() || !lobbyId.IsLobby()) return;
|
||||
|
||||
Debug.Log($"[Kcpable] LobbyEnter: {lobbyId}");
|
||||
ConnectionManager.Instance.SetCurrentLobby(lobbyId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_LobbyEnter error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.OnLobbyCreated(CSteamID)。
|
||||
/// 创建大厅时调用 ConnectionManager.SetCurrentLobby。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_LobbyCreated
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "OnLobbyCreated",
|
||||
new Type[] { typeof(CSteamID) });
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
public static void Postfix(CSteamID currentLobby)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable Harmony003 // CSteamID 值类型方法调用误报
|
||||
if (!currentLobby.IsValid() || !currentLobby.IsLobby()) return;
|
||||
#pragma warning restore Harmony003
|
||||
|
||||
Debug.Log($"[Kcpable] LobbyCreated: {currentLobby}");
|
||||
ConnectionManager.Instance.SetCurrentLobby(currentLobby);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_LobbyCreated error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.ClientInitLobbyAndOwner(CSteamID)。
|
||||
/// 加入他人大厅时调用 ConnectionManager.SetCurrentLobby。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_ClientInitLobby
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "ClientInitLobbyAndOwner",
|
||||
new Type[] { typeof(CSteamID) });
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
public static void Postfix(CSteamID lobby)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable Harmony003 // CSteamID 值类型方法调用误报
|
||||
if (!lobby.IsValid() || !lobby.IsLobby()) return;
|
||||
#pragma warning restore Harmony003
|
||||
|
||||
Debug.Log($"[Kcpable] ClientInitLobby: {lobby}");
|
||||
ConnectionManager.Instance.SetCurrentLobby(lobby);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_ClientInitLobby error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.OnLobbyDataUpdate(LobbyDataUpdate_t)。
|
||||
/// Lobby 数据更新时刷新成员列表。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_LobbyDataUpdate
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "OnLobbyDataUpdate",
|
||||
new Type[] { typeof(LobbyDataUpdate_t) });
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
public static void Postfix()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectionManager.Instance.RefreshLobbyMembers();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_LobbyDataUpdate error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.OnLobbyChatUpdate(LobbyChatUpdate_t)。
|
||||
/// 成员进入/离开时刷新成员列表。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_LobbyChatUpdate
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "OnLobbyChatUpdate",
|
||||
new Type[] { typeof(LobbyChatUpdate_t) });
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
public static void Postfix()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectionManager.Instance.RefreshLobbyMembers();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_LobbyChatUpdate error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 MatchmakingHandler.Disconnect(bool)。
|
||||
/// 离开大厅时清理所有 KCP 连接状态。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_LobbyDisconnect
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("MatchmakingHandler");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "Disconnect",
|
||||
new Type[] { typeof(bool) });
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
public static void Prefix()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.Log("[Kcpable] Disconnect: clearing lobby");
|
||||
ConnectionManager.Instance.ClearLobby();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_LobbyDisconnect error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
121
Patches/Patch_OnlinePlayerUI_KcpIndicator.cs
Normal file
121
Patches/Patch_OnlinePlayerUI_KcpIndicator.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// 内部辅助类:对 OnlinePlayerUI 实例遍历 mClients,为 KCP 已连接的对端在名前追加紫色 [K]。
|
||||
/// </summary>
|
||||
internal static class KcpIndicatorHelper
|
||||
{
|
||||
private const string KcpMarker = "<color=#9B59B6>[K]</color> ";
|
||||
|
||||
private static FieldInfo _mClientsField;
|
||||
private static FieldInfo _mPlayerTextsField;
|
||||
private static FieldInfo _clientIdField;
|
||||
|
||||
public static void AppendIndicator(object onlinePlayerUI)
|
||||
{
|
||||
if (onlinePlayerUI == null) return;
|
||||
|
||||
// 懒初始化反射缓存
|
||||
if (_mClientsField == null)
|
||||
{
|
||||
_mClientsField = AccessTools.Field(AccessTools.TypeByName("OnlinePlayerUI"), "mClients");
|
||||
_mPlayerTextsField = AccessTools.Field(AccessTools.TypeByName("OnlinePlayerUI"), "mPlayerTexts");
|
||||
_clientIdField = AccessTools.Field(AccessTools.TypeByName("ConnectedClientData"), "ClientID");
|
||||
}
|
||||
|
||||
if (_mClientsField == null || _mPlayerTextsField == null || _clientIdField == null)
|
||||
return;
|
||||
|
||||
var mClients = _mClientsField.GetValue(onlinePlayerUI) as Array;
|
||||
var mPlayerTexts = _mPlayerTextsField.GetValue(onlinePlayerUI) as TextMeshProUGUI[];
|
||||
if (mClients == null || mPlayerTexts == null) return;
|
||||
|
||||
int count = Math.Min(mClients.Length, mPlayerTexts.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var client = mClients.GetValue(i);
|
||||
if (client == null) continue;
|
||||
|
||||
var clientId = (CSteamID)_clientIdField.GetValue(client);
|
||||
if (!clientId.IsValid()) continue;
|
||||
|
||||
if (ConnectionManager.Instance.IsKcpConnected(clientId))
|
||||
{
|
||||
var text = mPlayerTexts[i].text;
|
||||
if (!text.Contains("[K]"))
|
||||
{
|
||||
mPlayerTexts[i].text = KcpMarker + text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: Hook OnlinePlayerUI.Update()。
|
||||
/// 每帧设置 mPlayerTexts[i].text = PlayerName,在此之后追加 [K] 标识。
|
||||
/// 使用 [HarmonyPriority(Priority.VeryLow)] 保证 Postfix 在 CN mod 之前执行。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_OnlinePlayerUI_Update_KcpIndicator
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("OnlinePlayerUI");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "Update", Type.EmptyTypes);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPriority(Priority.VeryLow)]
|
||||
public static void Postfix(object __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
KcpIndicatorHelper.AppendIndicator(__instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_OnlinePlayerUI_Update_KcpIndicator error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: Hook OnlinePlayerUI.Populate()。
|
||||
/// 初始化时设置 mPlayerTexts[i].text = PlayerName + Ping,在此之后追加 [K] 标识。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_OnlinePlayerUI_Populate_KcpIndicator
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("OnlinePlayerUI");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "Populate", Type.EmptyTypes);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPriority(Priority.VeryLow)]
|
||||
public static void Postfix(object __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
KcpIndicatorHelper.AppendIndicator(__instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Kcpable] Patch_OnlinePlayerUI_Populate_KcpIndicator error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Patches/Patch_P2PStatistics_Awake.cs
Normal file
67
Patches/Patch_P2PStatistics_Awake.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using HarmonyLib;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 修复 P2PStatistics.Awake() 因静态字典重复 Add 导致的崩溃。
|
||||
///
|
||||
/// 问题背景:
|
||||
/// P2PStatistics.Awake() 会遍历 P2PPackageHandler.MsgType 枚举所有值,
|
||||
/// 往 static Dictionary 中 Add 条目。若 Awake 被调用超过一次(场景重载等),
|
||||
/// 第二次调用时字典已有所有条目,Add 会抛 ArgumentException。
|
||||
///
|
||||
/// 本 Patch 在 Prefix 中检查 static Dictionary 是否已填充,若已填充则跳过 Awake。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_P2PStatistics_Awake
|
||||
{
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("P2PStatistics");
|
||||
if (type == null) return null;
|
||||
return AccessTools.Method(type, "Awake");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefix: 检查 static mPackageTypeStats 字典是否已包含条目。
|
||||
/// 若已填充则跳过原始 Awake,避免重复 Add 异常。
|
||||
/// </summary>
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix()
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = AccessTools.TypeByName("P2PStatistics");
|
||||
if (type == null) return true;
|
||||
|
||||
var dictField = AccessTools.Field(type, "mPackageTypeStats");
|
||||
if (dictField == null) return true;
|
||||
|
||||
// 尝试以多种方式读取字典的 Count
|
||||
var dict = dictField.GetValue(null);
|
||||
if (dict == null) return true;
|
||||
|
||||
// 反射获取 Count 属性
|
||||
var countProp = dict.GetType().GetProperty("Count");
|
||||
if (countProp != null)
|
||||
{
|
||||
int count = (int)countProp.GetValue(dict, null);
|
||||
if (count > 0)
|
||||
{
|
||||
// 字典已填充,跳过 Awake
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable] Patch_P2PStatistics_Awake error (fallback to original): {ex.Message}");
|
||||
}
|
||||
|
||||
return true; // 运行原始 Awake
|
||||
}
|
||||
}
|
||||
73
Patches/Patch_ReadMessageBuffer_Hello.cs
Normal file
73
Patches/Patch_ReadMessageBuffer_Hello.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 P2PPackageHandler.ReadMessageBuffer,在 ClientInit 之前也放行 Hello (75)。
|
||||
///
|
||||
/// 问题背景:
|
||||
/// ReadMessageBuffer 在 HasBeenInitializedFromServer == false 时只放行 ClientInit (4) 和
|
||||
/// ClientAccepted (5),其他 MsgType(包括我们的 Hello=75)会被丢弃。
|
||||
/// 这导致 KCP Hello 握手永远无法完成,两端 KCP 连接永远无法建立。
|
||||
///
|
||||
/// 本 Patch 在 Prefix 中检测 msgType==75,直接调用 CheckMessageType 处理 Hello,
|
||||
/// 跳过原始方法中的前置过滤。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_ReadMessageBuffer_Hello
|
||||
{
|
||||
private static MethodInfo _checkMessageTypeMethod;
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("P2PPackageHandler");
|
||||
if (type == null) return null;
|
||||
var msgTypeEnum = AccessTools.TypeByName("P2PPackageHandler+MsgType");
|
||||
_checkMessageTypeMethod = AccessTools.Method(type, "CheckMessageType",
|
||||
new Type[] { typeof(byte[]), msgTypeEnum, typeof(CSteamID) });
|
||||
return AccessTools.Method(type, "ReadMessageBuffer",
|
||||
new Type[] { typeof(byte[]), typeof(CSteamID) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefix: 若 rawData 中的 msgType 为 Hello (75),绕过原始 ReadMessageBuffer 过滤,
|
||||
/// 直接调用 CheckMessageType 处理。
|
||||
/// </summary>
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(object __instance, byte[] rawData, CSteamID SteamIdRemote)
|
||||
{
|
||||
// rawData 格式: [4 bytes timestamp] [1 byte msgType] [payload...]
|
||||
if (rawData == null || rawData.Length < 5)
|
||||
return true;
|
||||
|
||||
byte msgTypeByte = rawData[4];
|
||||
if (msgTypeByte != (byte)MsgType.Hello)
|
||||
return true; // 非 Hello,走原始逻辑
|
||||
|
||||
if (_checkMessageTypeMethod == null)
|
||||
{
|
||||
UnityEngine.Debug.LogError("[Kcpable] CheckMessageType method not found, cannot process Hello!");
|
||||
return true; // 回退到原始逻辑(会被丢弃,但不抛异常)
|
||||
}
|
||||
|
||||
// 提取 payload(跳过 5 字节头部)
|
||||
int payloadLen = rawData.Length - 5;
|
||||
byte[] payload = new byte[payloadLen];
|
||||
if (payloadLen > 0)
|
||||
Array.Copy(rawData, 5, payload, 0, payloadLen);
|
||||
|
||||
// 构造 P2PPackageHandler.MsgType 枚举值 75
|
||||
var msgTypeEnum = AccessTools.TypeByName("P2PPackageHandler+MsgType");
|
||||
object helloMsgType = Enum.ToObject(msgTypeEnum, (int)MsgType.Hello);
|
||||
|
||||
// 直接调用 CheckMessageType(payload, Hello, SteamIdRemote)
|
||||
_checkMessageTypeMethod.Invoke(__instance, new object[] { payload, helloMsgType, SteamIdRemote });
|
||||
|
||||
return false; // 跳过原始 ReadMessageBuffer
|
||||
}
|
||||
}
|
||||
96
Patches/Patch_SendP2PPacketToUser.cs
Normal file
96
Patches/Patch_SendP2PPacketToUser.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 P2PPackageHandler.SendP2PPacketToUser(CSteamID, ...)。
|
||||
/// 根据 effectiveMode 决定走 KCP 或保留原 Steam 发送。
|
||||
/// 使用 [HarmonyTargetMethod] 动态查找目标方法(因为 MsgType 是嵌套枚举)。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_SendP2PPacketToUser
|
||||
{
|
||||
private static MethodInfo _writeMessageBufferMethod;
|
||||
private static Type _p2pHandlerType;
|
||||
|
||||
#if DEBUG
|
||||
private static ulong _totalKcpTaken = 0;
|
||||
private static ulong _totalSteamFallback = 0;
|
||||
private static ulong _totalConservativeBypass = 0;
|
||||
#endif
|
||||
|
||||
private static bool ResolveTypes()
|
||||
{
|
||||
if (_p2pHandlerType != null) return true;
|
||||
_p2pHandlerType = AccessTools.TypeByName("P2PPackageHandler");
|
||||
if (_p2pHandlerType == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning("[Kcpable] P2PPackageHandler type not found, send patch disabled.");
|
||||
return false;
|
||||
}
|
||||
_writeMessageBufferMethod = AccessTools.Method(_p2pHandlerType, "WriteMessageBuffer");
|
||||
return _writeMessageBufferMethod != null;
|
||||
}
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("P2PPackageHandler");
|
||||
if (type == null) return null;
|
||||
var msgTypeEnum = AccessTools.TypeByName("P2PPackageHandler+MsgType");
|
||||
return AccessTools.Method(type, "SendP2PPacketToUser",
|
||||
new Type[] { typeof(CSteamID), typeof(byte[]), msgTypeEnum, typeof(EP2PSend), typeof(int) });
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(object __instance, CSteamID clientID, byte[] data,
|
||||
object messageType, EP2PSend sendMethod, int channel)
|
||||
{
|
||||
if (!ResolveTypes()) return true;
|
||||
|
||||
var mgr = ConnectionManager.Instance;
|
||||
var effectiveMode = mgr.GetEffectiveMode(clientID);
|
||||
|
||||
#if DEBUG
|
||||
int msgTypeVal = (int)Convert.ChangeType(messageType, typeof(int));
|
||||
#pragma warning disable Harmony003 // CSteamID 值类型,仅读取不修改
|
||||
var sid = clientID.m_SteamID;
|
||||
#pragma warning restore Harmony003
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacketToUser ch={channel} to={sid} msgType={msgTypeVal} send={sendMethod} mode={effectiveMode}");
|
||||
#endif
|
||||
|
||||
if (effectiveMode == KcpMode.Conservative)
|
||||
{
|
||||
if (sendMethod == EP2PSend.k_EP2PSendUnreliable ||
|
||||
sendMethod == EP2PSend.k_EP2PSendUnreliableNoDelay)
|
||||
{
|
||||
#if DEBUG
|
||||
_totalConservativeBypass++;
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacketToUser ch={channel} → Conservative Unreliable BYPASS (x{_totalConservativeBypass})");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] rawPacket = (byte[])_writeMessageBufferMethod.Invoke(__instance, new object[] { data, messageType });
|
||||
if (mgr.TrySend(clientID, channel, rawPacket, rawPacket.Length))
|
||||
{
|
||||
#if DEBUG
|
||||
_totalKcpTaken++;
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacketToUser ch={channel} → KCP TOOK OVER (x{_totalKcpTaken})");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
_totalSteamFallback++;
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacketToUser ch={channel} → KCP not ready, Steam fallback (x{_totalSteamFallback})");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
233
Patches/Patch_SteamNetworking_Recv.cs
Normal file
233
Patches/Patch_SteamNetworking_Recv.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
// 预编译委托:消除 MethodInfo.Invoke 的装箱/拆箱开销
|
||||
delegate bool IsP2PPacketAvailableDelegate(out uint msgSize, int channel);
|
||||
delegate bool ReadP2PPacketDelegate(byte[] dest, uint cubDest, out uint msgSize, out CSteamID sender, int channel);
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 SteamNetworking.IsP2PPacketAvailable。
|
||||
///
|
||||
/// 使用 [ThreadStatic] _isPumping 标志位防止递归:
|
||||
/// PumpKcpFromSteam 内调用 isAvail/read 时也会触发本 Prefix,
|
||||
/// 此时 _isPumping = true,Prefix 直接放行到原始 Steam API,
|
||||
/// 不再二次进入 PumpKcpFromSteam。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_IsP2PPacketAvailable
|
||||
{
|
||||
private static Type SteamNetType => AccessTools.TypeByName("SteamNetworking");
|
||||
|
||||
// 预编译委托,初始化时一次性创建,之后零分配直接调用
|
||||
private static IsP2PPacketAvailableDelegate _isAvailDel;
|
||||
private static ReadP2PPacketDelegate _readDel;
|
||||
|
||||
/// <summary>
|
||||
/// 线程本地标志:当前是否正在 PumpKcpFromSteam 中执行。
|
||||
/// 用于阻止 PumpKcpFromSteam 内部调用 Steam API 时再次触发 Prefix → 无限递归。
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static bool _isPumping;
|
||||
|
||||
#if DEBUG
|
||||
private static int _recursionDepth = 0;
|
||||
#endif
|
||||
|
||||
private static IsP2PPacketAvailableDelegate IsAvailDel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isAvailDel == null)
|
||||
{
|
||||
var method = AccessTools.Method(SteamNetType, "IsP2PPacketAvailable",
|
||||
new Type[] { typeof(uint).MakeByRefType(), typeof(int) });
|
||||
if (method != null)
|
||||
_isAvailDel = (IsP2PPacketAvailableDelegate)Delegate.CreateDelegate(
|
||||
typeof(IsP2PPacketAvailableDelegate), method);
|
||||
}
|
||||
return _isAvailDel;
|
||||
}
|
||||
}
|
||||
|
||||
private static ReadP2PPacketDelegate ReadDel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_readDel == null)
|
||||
{
|
||||
var method = AccessTools.Method(SteamNetType, "ReadP2PPacket",
|
||||
new Type[] { typeof(byte[]), typeof(uint), typeof(uint).MakeByRefType(),
|
||||
typeof(CSteamID).MakeByRefType(), typeof(int) });
|
||||
if (method != null)
|
||||
_readDel = (ReadP2PPacketDelegate)Delegate.CreateDelegate(
|
||||
typeof(ReadP2PPacketDelegate), method);
|
||||
}
|
||||
return _readDel;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
return AccessTools.Method(SteamNetType, "IsP2PPacketAvailable",
|
||||
new Type[] { typeof(uint).MakeByRefType(), typeof(int) });
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(ref bool __result, out uint pcubMsgSize, int nChannel)
|
||||
{
|
||||
// ── 递归保护:若当前已在 PumpKcpFromSteam 内,直接放行到原始 Steam API ──
|
||||
if (_isPumping)
|
||||
{
|
||||
pcubMsgSize = 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
_recursionDepth++;
|
||||
if (_recursionDepth > 5)
|
||||
UnityEngine.Debug.LogError($"[Kcpable|DEBUG] IsP2PPacketAvailable RECURSION depth={_recursionDepth} channel={nChannel} — still recursing!");
|
||||
else if (_recursionDepth > 2)
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable|DEBUG] IsP2PPacketAvailable depth={_recursionDepth} channel={nChannel}");
|
||||
else;
|
||||
//UnityEngine.Debug.Log($"[Kcpable|DEBUG] IsP2PPacketAvailable ENTER depth={_recursionDepth} channel={nChannel}");
|
||||
#endif
|
||||
|
||||
// 1. 先检查 KCP 解码缓冲区
|
||||
if (DecodedPacketBuffer.Instance.TryPeekSize(nChannel, out int pendingSize))
|
||||
{
|
||||
pcubMsgSize = (uint)pendingSize;
|
||||
__result = true;
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] IsP2PPacketAvailable channel={nChannel} → DecodedPacketBuffer HIT, size={pendingSize}");
|
||||
_recursionDepth--;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 从 Steam offset channel 读取 raw KCP 数据并解码
|
||||
int kcpChannel = ConnectionManager.ToKcpChannel(nChannel);
|
||||
PumpKcpFromSteam(nChannel, kcpChannel);
|
||||
|
||||
// 3. 再次检查解码缓冲区
|
||||
if (DecodedPacketBuffer.Instance.TryPeekSize(nChannel, out pendingSize))
|
||||
{
|
||||
pcubMsgSize = (uint)pendingSize;
|
||||
__result = true;
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] IsP2PPacketAvailable channel={nChannel} → after PumpKcp, buffer HIT size={pendingSize}");
|
||||
_recursionDepth--;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 回退:让游戏检查原始 channel(兼容无 KCP 对端)
|
||||
pcubMsgSize = 0u;
|
||||
#if DEBUG
|
||||
//UnityEngine.Debug.Log($"[Kcpable|DEBUG] IsP2PPacketAvailable channel={nChannel} → FALLBACK to original Steam");
|
||||
_recursionDepth--;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PumpKcpFromSteam(int origChannel, int kcpChannel)
|
||||
{
|
||||
var isAvail = IsAvailDel;
|
||||
var read = ReadDel;
|
||||
|
||||
if (isAvail == null || read == null)
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → delegate null, ABORT");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
int pumpCount = 0;
|
||||
//UnityEngine.Debug.Log($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → reading raw KCP packets...");
|
||||
#endif
|
||||
|
||||
// 设置递归保护标志:此后本线程内所有 Steam API 调用直接放行
|
||||
_isPumping = true;
|
||||
try
|
||||
{
|
||||
while (isAvail(out uint rawSize, kcpChannel))
|
||||
{
|
||||
byte[] raw = new byte[rawSize];
|
||||
if (!read(raw, rawSize, out _, out CSteamID sender, kcpChannel))
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.LogError($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → ReadP2PPacket FAILED!");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
pumpCount++;
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → raw pkt {pumpCount}: {rawSize}B from {sender.m_SteamID}");
|
||||
#endif
|
||||
ConnectionManager.Instance.OnKcpDataReceived(sender, origChannel, raw, 0, (int)rawSize);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isPumping = false;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (pumpCount == 0)
|
||||
;
|
||||
//UnityEngine.Debug.Log($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → no raw packets found");
|
||||
else
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] PumpKcpFromSteam orig={origChannel} kcp={kcpChannel} → pumped {pumpCount} raw KCP packets total");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 SteamNetworking.ReadP2PPacket。
|
||||
/// 从 KCP 解码缓冲区出队已解码的数据返回给调用者。
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_ReadP2PPacket
|
||||
{
|
||||
private static Type SteamNetType => AccessTools.TypeByName("SteamNetworking");
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
return AccessTools.Method(SteamNetType, "ReadP2PPacket",
|
||||
new Type[] { typeof(byte[]), typeof(uint), typeof(uint).MakeByRefType(),
|
||||
typeof(CSteamID).MakeByRefType(), typeof(int) });
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(ref bool __result, byte[] pubDest, uint cubDest,
|
||||
out uint pcubMsgSize, out CSteamID psteamIDRemote, int nChannel)
|
||||
{
|
||||
if (DecodedPacketBuffer.Instance.TryDequeue(nChannel, out psteamIDRemote, out byte[] data))
|
||||
{
|
||||
int copyLen = Math.Min(data.Length, (int)cubDest);
|
||||
Array.Copy(data, 0, pubDest, 0, copyLen);
|
||||
pcubMsgSize = (uint)copyLen;
|
||||
__result = true;
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] ReadP2PPacket channel={nChannel} → KCP decoded: {copyLen}B from {psteamIDRemote.m_SteamID}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
pcubMsgSize = 0u;
|
||||
psteamIDRemote = default;
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] ReadP2PPacket channel={nChannel} → no KCP data, fallback to original Steam read");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
130
Patches/Patch_SteamNetworking_Send.cs
Normal file
130
Patches/Patch_SteamNetworking_Send.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace stick.plugins.kcpable.Patches;
|
||||
|
||||
/// <summary>
|
||||
/// Harmony Patch: 拦截 SteamNetworking.SendP2PPacket(最底层发送 API)。
|
||||
/// 这是所有 Steam P2P 发送的最终出口,覆盖 P2PPackageHandler、MultiplayerManager、
|
||||
/// NetworkPlayer、SyncableObjectManager 等所有调用方。
|
||||
///
|
||||
/// 与 Patch_SendP2PPacketToUser 的协作:
|
||||
/// - SendP2PPacketToUser 拦截在 P2PPackageHandler 层(Game→Steam 的语义层),
|
||||
/// 可访问 MsgType/WriteMessageBuffer 做编码
|
||||
/// - 本 Patch 拦截在 Steam API 层(最底层),捕获所有绕过 P2PPackageHandler
|
||||
/// 直接调用 SteamNetworking 的发送路径(如 MultiplayerManager.OnPlayerMoved 等)
|
||||
/// - 两 Patch 不冲突:SendP2PPacketToUser 成功 KCP 接管时,原方法不执行,
|
||||
/// SteamNetworking.SendP2PPacket 不被调用;失败回退时,本 Patch 做一致性判断
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class Patch_SteamNetworking_Send
|
||||
{
|
||||
private static Type SteamNetType => AccessTools.TypeByName("SteamNetworking");
|
||||
|
||||
#if DEBUG
|
||||
private static ulong _totalKcpTookover = 0;
|
||||
private static ulong _totalSteamPassthrough = 0;
|
||||
private static ulong _totalKcpInternal = 0;
|
||||
#endif
|
||||
|
||||
[HarmonyTargetMethod]
|
||||
public static MethodBase TargetMethod()
|
||||
{
|
||||
return AccessTools.Method(SteamNetType, "SendP2PPacket",
|
||||
new Type[] { typeof(CSteamID), typeof(byte[]), typeof(uint),
|
||||
typeof(EP2PSend), typeof(int) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefix: 决定是否将此次 Steam 发送升级为 KCP。
|
||||
/// 返回 false = KCP 已接管,跳过原始 Steam 发送。
|
||||
/// 返回 true = 走原始 Steam 发送(KCP 未就绪 / 模式不匹配 / KCP 内部流量)。
|
||||
/// </summary>
|
||||
[HarmonyPrefix]
|
||||
public static bool Prefix(CSteamID steamIDRemote, byte[] pubData, uint cubData,
|
||||
EP2PSend eP2PSendType, int nChannel)
|
||||
{
|
||||
#if DEBUG
|
||||
#pragma warning disable Harmony003 // CSteamID 值类型,仅读取不修改
|
||||
var steamID = steamIDRemote.m_SteamID;
|
||||
#pragma warning restore Harmony003
|
||||
#endif
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// 1. KCP 内部流量:channel >= 10000 → 永不过滤
|
||||
// KCP 输出回调自身使用 Unreliable + channel+OFFSET 发送底层数据
|
||||
// ────────────────────────────────────────
|
||||
if (nChannel >= Const.KCP_CHANNEL_OFFSET)
|
||||
{
|
||||
#if DEBUG
|
||||
_totalKcpInternal++;
|
||||
if (_totalKcpInternal <= 3 || _totalKcpInternal % 200 == 0)
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacket channel={nChannel} → KCP INTERNAL (pass x{_totalKcpInternal})");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// 2. 查询对该对端生效的 KcpMode
|
||||
// ────────────────────────────────────────
|
||||
var mgr = ConnectionManager.Instance;
|
||||
var effectiveMode = mgr.GetEffectiveMode(steamIDRemote);
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// 3. Conservative 模式:Unreliable* 直通原始 Steam
|
||||
// ────────────────────────────────────────
|
||||
if (effectiveMode == KcpMode.Conservative)
|
||||
{
|
||||
if (eP2PSendType == EP2PSend.k_EP2PSendUnreliable ||
|
||||
eP2PSendType == EP2PSend.k_EP2PSendUnreliableNoDelay)
|
||||
{
|
||||
#if DEBUG
|
||||
_totalSteamPassthrough++;
|
||||
if (_totalSteamPassthrough <= 5 || _totalSteamPassthrough % 200 == 0)
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacket ch={nChannel} to={steamID} send={eP2PSendType} → Conservative Unreliable PASSTHROUGH (x{_totalSteamPassthrough})");
|
||||
#endif
|
||||
return true; // Steam 直通
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// 4. 尝试路由到 KCP
|
||||
// pubData 可能来自 WriteMessageBuffer 编码(经 P2PPackageHandler)
|
||||
// 也可能来自 MultiplayerManager 等直接调用方的自有格式
|
||||
// KCP 作为透明传输层,不关心数据格式
|
||||
// ────────────────────────────────────────
|
||||
// 截取有效数据长度(数组可能比 cubData 大)
|
||||
int length = (int)Math.Min(cubData, (uint)(pubData?.Length ?? 0));
|
||||
if (length == 0)
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable|DEBUG] SendP2PPacket ch={nChannel} to={steamID} → ZERO length data!");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacket ch={nChannel} to={steamID} send={eP2PSendType} mode={effectiveMode} len={length} → trying KCP takeover...");
|
||||
#endif
|
||||
|
||||
if (mgr.TrySend(steamIDRemote, nChannel, pubData, length))
|
||||
{
|
||||
#if DEBUG
|
||||
_totalKcpTookover++;
|
||||
if (_totalKcpTookover <= 10 || _totalKcpTookover % 100 == 0)
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacket ch={nChannel} to={steamID} → KCP TOOK OVER (x{_totalKcpTookover})");
|
||||
#endif
|
||||
return false; // KCP 已接管,跳过原始 Steam 发送
|
||||
}
|
||||
|
||||
// KCP 未就绪(peer 不存在 / 未 Connected),回退到原始 Steam 发送
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] SendP2PPacket ch={nChannel} to={steamID} mode={effectiveMode} → KCP NOT READY, fallback to Steam");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
78
Plugin.cs
78
Plugin.cs
@@ -1,6 +1,10 @@
|
||||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using HarmonyLib;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Helper;
|
||||
using stick.plugins.kcpable.Patches;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -10,18 +14,80 @@ namespace stick.plugins.kcpable;
|
||||
[BepInProcess("StickFight.exe")]
|
||||
public class Plugin : BaseUnityPlugin
|
||||
{
|
||||
private ConfigEntry<string> _kcpModeConfig;
|
||||
private Harmony _harmony;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// BepInEx Config
|
||||
_kcpModeConfig = Config.Bind(
|
||||
"Kcpable",
|
||||
"KcpMode",
|
||||
"Conservative",
|
||||
"KCP upgrade mode:\n" +
|
||||
" Conservative = only upgrade Reliable channels (safe)\n" +
|
||||
" Aggressive = upgrade ALL channels including Unreliable (may cause micro-stutters on high packet loss)"
|
||||
);
|
||||
|
||||
if (!SteamManager.Initialized)
|
||||
{
|
||||
Logger.LogWarning("Steam not initialized, Kcpable will not be active.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析配置为 KcpMode
|
||||
var localMode = ParseMode(_kcpModeConfig.Value);
|
||||
ConnectionManager.Instance.Initialize(SteamUser.GetSteamID());
|
||||
ConnectionManager.Instance.LocalMode = localMode;
|
||||
|
||||
// Patch 层应在后续注册 SendHello / OnKcpOutput
|
||||
Logger.LogInfo("Kcpable initialized.");
|
||||
// 注册 KCP 输出回调:通过 Steam P2P Unreliable 发送到 channel + OFFSET
|
||||
ConnectionManager.Instance.OnKcpOutput = (peerId, channel, data, size) =>
|
||||
{
|
||||
int kcpChannel = ConnectionManager.ToKcpChannel(channel);
|
||||
SteamNetworking.SendP2PPacket(peerId, data, (uint)size,
|
||||
EP2PSend.k_EP2PSendUnreliable, kcpChannel);
|
||||
};
|
||||
|
||||
// 注册 Hello 发送回调:通过 Steam P2P 发送,格式兼容游戏消息解析
|
||||
// 游戏消息格式:[uint32 timestamp] [byte msgType] [bytes payload]
|
||||
// Hello: timestamp=ServerRealTime, msgType=75, payload=[version, mode]
|
||||
ConnectionManager.Instance.SendHello = (peerId, helloBytes) =>
|
||||
{
|
||||
uint timestamp = SteamUtils.GetServerRealTime();
|
||||
byte[] packet = new byte[5 + helloBytes.Length];
|
||||
using (var ms = new System.IO.MemoryStream(packet))
|
||||
using (var bw = new System.IO.BinaryWriter(ms))
|
||||
{
|
||||
bw.Write(timestamp);
|
||||
bw.Write((byte)Protocol.MsgType.Hello); // 75
|
||||
bw.Write(helloBytes); // [version, mode]
|
||||
}
|
||||
SteamNetworking.SendP2PPacket(peerId, packet, (uint)packet.Length,
|
||||
EP2PSend.k_EP2PSendReliable, 0);
|
||||
};
|
||||
|
||||
// 注册 Harmony 补丁
|
||||
_harmony = new Harmony("stick.plugins.kcpable");
|
||||
_harmony.PatchAll(typeof(Patch_SendP2PPacketToUser));
|
||||
_harmony.PatchAll(typeof(Patch_IsP2PPacketAvailable));
|
||||
_harmony.PatchAll(typeof(Patch_ReadP2PPacket));
|
||||
_harmony.PatchAll(typeof(Patch_CheckMessageType_Hello));
|
||||
_harmony.PatchAll(typeof(Patch_ReadMessageBuffer_Hello));
|
||||
_harmony.PatchAll(typeof(Patch_P2PStatistics_Awake));
|
||||
// Steam API 层拦截:覆盖所有绕过 P2PPackageHandler 的直接发送路径
|
||||
_harmony.PatchAll(typeof(Patch_SteamNetworking_Send));
|
||||
// 大厅事件 Hook
|
||||
_harmony.PatchAll(typeof(Patch_LobbyEnter));
|
||||
_harmony.PatchAll(typeof(Patch_LobbyCreated));
|
||||
_harmony.PatchAll(typeof(Patch_ClientInitLobby));
|
||||
_harmony.PatchAll(typeof(Patch_LobbyDataUpdate));
|
||||
_harmony.PatchAll(typeof(Patch_LobbyChatUpdate));
|
||||
_harmony.PatchAll(typeof(Patch_LobbyDisconnect));
|
||||
// [K] 标识 Hook
|
||||
_harmony.PatchAll(typeof(Patch_OnlinePlayerUI_Update_KcpIndicator));
|
||||
_harmony.PatchAll(typeof(Patch_OnlinePlayerUI_Populate_KcpIndicator));
|
||||
|
||||
Logger.LogInfo($"Kcpable initialized in {localMode} mode.");
|
||||
}
|
||||
|
||||
void Update()
|
||||
@@ -32,6 +98,14 @@ public class Plugin : BaseUnityPlugin
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
_harmony?.UnpatchSelf();
|
||||
ConnectionManager.Instance.Shutdown();
|
||||
}
|
||||
|
||||
private static KcpMode ParseMode(string value)
|
||||
{
|
||||
if (string.Equals(value, "Aggressive", System.StringComparison.OrdinalIgnoreCase))
|
||||
return KcpMode.Aggressive;
|
||||
return KcpMode.Conservative; // 默认保守模式
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,11 @@ namespace stick.plugins.kcpable.Protocol;
|
||||
public static class Const
|
||||
{
|
||||
public const byte version = 1;
|
||||
|
||||
/// <summary>
|
||||
/// KCP 传输层使用的 channel 偏移量。
|
||||
/// 游戏读写 channel X,KCP 在 Steam 层实际使用 channel X + OFFSET。
|
||||
/// 10000 远大于游戏最大 channel (约 2*maxPlayers+2),确保永不冲突。
|
||||
/// </summary>
|
||||
public const int KCP_CHANNEL_OFFSET = 10000;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
using Lidgren.Network;
|
||||
using stick.plugins.kcpable.Transport;
|
||||
|
||||
namespace stick.plugins.kcpable.Protocol;
|
||||
|
||||
// 在懒得发的原有网络栈上握手使用的Hello消息
|
||||
/// <summary>
|
||||
/// Hello 握手消息,纯粹的数据传输对象。
|
||||
/// 格式:[version:1 byte] [mode:1 byte]
|
||||
/// mode 声明本地的 KcpMode,对端取 min 自动降级,无需额外握手。
|
||||
/// </summary>
|
||||
public class Hello
|
||||
{
|
||||
public byte version = Const.version;
|
||||
public KcpMode mode = KcpMode.Conservative;
|
||||
|
||||
// 暂时懒得实现重试和超时
|
||||
// 事实上我甚至没想好要不要在这层实现
|
||||
// 先留个可选参数
|
||||
public void Send(P2PPackageHandler p2pHandler, NetConnection user, ushort retryTimes = 3, ulong timeoutMs = 5000)
|
||||
/// <summary>
|
||||
/// 从原始字节数组解析 Hello 包。
|
||||
/// 兼容旧版 1 字节 Hello(默认视为 Conservative)。
|
||||
/// </summary>
|
||||
public static Hello FromBytes(byte[] data)
|
||||
{
|
||||
p2pHandler.SendSocketP2PPacketToUser(user, [version], (P2PPackageHandler.MsgType)MsgType.Hello);
|
||||
var hello = new Hello();
|
||||
if (data.Length >= 1)
|
||||
hello.version = data[0];
|
||||
if (data.Length >= 2)
|
||||
hello.mode = (KcpMode)data[1];
|
||||
// data.Length == 1 时 mode 保持默认 Conservative
|
||||
return hello;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 Hello 包的字节数组(用于 SendHello 回调)。
|
||||
/// </summary>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
return [version, (byte)mode];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Helper;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.kcpable.Transport;
|
||||
@@ -18,17 +19,29 @@ public class ConnectionManager
|
||||
private CSteamID? _currentLobbyId;
|
||||
private readonly Dictionary<CSteamID, PeerConnection> _peers = new();
|
||||
|
||||
/// <summary>
|
||||
/// 本地配置的 KcpMode,由 Plugin 从 BepInEx Config 读取后设置。
|
||||
/// </summary>
|
||||
public KcpMode LocalMode { get; set; } = KcpMode.Conservative;
|
||||
|
||||
/// <summary>
|
||||
/// Patch 层注册:当 KCP 需要发送底层 UDP 数据时触发。
|
||||
/// 参数:peerSteamId, channel, buffer, size
|
||||
/// 参数:peerSteamId, originalChannel, buffer, size
|
||||
/// Patch 层应使用 originalChannel + KCP_CHANNEL_OFFSET 调用 Steam SendP2PPacket。
|
||||
/// </summary>
|
||||
public Action<CSteamID, int, byte[], int> OnKcpOutput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Patch 层注册:当需要发送 Hello 握手包时触发。
|
||||
/// Hello 内容固定(version 字节),Patch 层只需通过原 TCP 栈发给指定对端。
|
||||
/// 参数:peerSteamId, helloBytes (包含 version + mode)
|
||||
/// </summary>
|
||||
public Action<CSteamID> SendHello { get; set; }
|
||||
public Action<CSteamID, byte[]> SendHello { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 将游戏 channel 映射为 KCP 底层传输 channel。
|
||||
/// KCP 数据在 Steam 层使用 channel + KCP_CHANNEL_OFFSET,避免与游戏原始包冲突。
|
||||
/// </summary>
|
||||
public static int ToKcpChannel(int channel) => channel + Const.KCP_CHANNEL_OFFSET;
|
||||
|
||||
public void Initialize(CSteamID localSteamId)
|
||||
{
|
||||
@@ -39,6 +52,7 @@ public class ConnectionManager
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// ClearLobby() 内部已调用 DecodedPacketBuffer.Instance.Clear()
|
||||
ClearLobby();
|
||||
}
|
||||
|
||||
@@ -66,11 +80,11 @@ public class ConnectionManager
|
||||
}
|
||||
_peers.Clear();
|
||||
_currentLobbyId = null;
|
||||
DecodedPacketBuffer.Instance.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch 层可选调用:在收到 lobby 数据更新事件时立即刷新一次成员列表。
|
||||
/// 核心逻辑不依赖此调用,因为 Update 中已经每帧轮询。
|
||||
/// </summary>
|
||||
public void RefreshLobbyMembers()
|
||||
{
|
||||
@@ -80,64 +94,132 @@ public class ConnectionManager
|
||||
|
||||
/// <summary>
|
||||
/// Patch 层收到 Hello 包时调用。
|
||||
/// 无论当前状态是 Handshaking 还是 Idle,一律升级为 Connected。
|
||||
/// 解析对端 version + mode,注入 LocalMode 后标记 Connected。
|
||||
/// </summary>
|
||||
public void OnHelloReceived(CSteamID fromPeer)
|
||||
public void OnHelloReceived(CSteamID fromPeer, Hello hello)
|
||||
{
|
||||
if (fromPeer == _localSteamId)
|
||||
return;
|
||||
|
||||
if (!_peers.TryGetValue(fromPeer, out var peer))
|
||||
{
|
||||
// 对端先发了 Hello,但本地尚未通过大厅同步发现该玩家
|
||||
// 直接创建并标记为 Connected
|
||||
peer = new PeerConnection(fromPeer, _localSteamId, OnPeerKcpOutput);
|
||||
peer = new PeerConnection(fromPeer, _localSteamId, OnPeerKcpOutput, Time.time);
|
||||
_peers[fromPeer] = peer;
|
||||
}
|
||||
|
||||
peer.LocalMode = LocalMode;
|
||||
peer.RemoteMode = hello.mode;
|
||||
peer.MarkConnected();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询与指定对端实际生效的 KcpMode。
|
||||
/// 若对端不存在或未 Connected,返回 Conservative。
|
||||
/// </summary>
|
||||
public KcpMode GetEffectiveMode(CSteamID peerId)
|
||||
{
|
||||
if (_peers.TryGetValue(peerId, out var peer) && peer.State == PeerState.Connected)
|
||||
return peer.EffectiveMode;
|
||||
return KcpMode.Conservative;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询对端是否已建立 KCP 连接(State == Connected)。
|
||||
/// 用于 UI 层判断是否显示 [K] 标识等。
|
||||
/// </summary>
|
||||
public bool IsKcpConnected(CSteamID peerId)
|
||||
{
|
||||
return _peers.TryGetValue(peerId, out var peer)
|
||||
&& peer.State == PeerState.Connected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch 层收到 KCP 底层 UDP 数据时调用。
|
||||
/// 解码后自动入队 DecodedPacketBuffer。
|
||||
/// </summary>
|
||||
public void OnKcpDataReceived(CSteamID peerId, int channel, byte[] data, int offset, int length)
|
||||
{
|
||||
if (!_peers.TryGetValue(peerId, out var peer))
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable|DEBUG] OnKcpDataReceived ch={channel} from={peerId.m_SteamID} → peer NOT FOUND, data DROPPED ({length}B)");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (peer.State != PeerState.Connected)
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.LogWarning($"[Kcpable|DEBUG] OnKcpDataReceived ch={channel} from={peerId.m_SteamID} → peer state={peer.State}, data DROPPED ({length}B)");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peer.KcpChannels.TryGetValue(channel, out var kcp))
|
||||
return;
|
||||
|
||||
var kcp = peer.EnsureKcpForChannel(channel);
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] OnKcpDataReceived ch={channel} from={peerId.m_SteamID} → kcp.Input {length}B");
|
||||
#endif
|
||||
kcp.Input(data, offset, length);
|
||||
|
||||
// 尝试从 KCP 解码并放入 DecodedPacketBuffer
|
||||
int decodedCount = 0;
|
||||
while (true)
|
||||
{
|
||||
int peekSize = kcp.PeekSize();
|
||||
if (peekSize <= 0)
|
||||
break;
|
||||
|
||||
byte[] buf = new byte[peekSize];
|
||||
int recvLen = kcp.Receive(buf, buf.Length);
|
||||
if (recvLen > 0)
|
||||
{
|
||||
DecodedPacketBuffer.Instance.Enqueue(channel, peerId, buf);
|
||||
decodedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
if (decodedCount > 0)
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] OnKcpDataReceived ch={channel} from={peerId.m_SteamID} → decoded {decodedCount} packets from KCP");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏逻辑尝试发送数据时调用。
|
||||
/// 若对端已 Connected,数据进入 KCP 发送队列并返回 true,Patch 层应拦截原 TCP 发送。
|
||||
/// 若未就绪,返回 false,Patch 层继续走原 TCP 栈。
|
||||
/// 若对端已 Connected,数据进入 KCP 发送队列并返回 true。
|
||||
/// 若未就绪,返回 false,Patch 层继续走原 Steam 栈。
|
||||
/// </summary>
|
||||
public bool TrySend(CSteamID peerId, int channel, byte[] data, int length)
|
||||
{
|
||||
if (!_peers.TryGetValue(peerId, out var peer))
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] TrySend ch={channel} to={peerId.m_SteamID} → peer NOT FOUND ({length}B)");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (peer.State != PeerState.Connected)
|
||||
{
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] TrySend ch={channel} to={peerId.m_SteamID} → peer state={peer.State} ({length}B)");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!peer.KcpChannels.TryGetValue(channel, out var kcp))
|
||||
return false;
|
||||
|
||||
var kcp = peer.EnsureKcpForChannel(channel);
|
||||
kcp.Send(data, 0, length);
|
||||
#if DEBUG
|
||||
UnityEngine.Debug.Log($"[Kcpable|DEBUG] TrySend ch={channel} to={peerId.m_SteamID} → kcp.Send {length}B OK");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每帧调用:驱动 Hello 冗余广播调度 + KCP Update。
|
||||
/// 大厅成员同步由 Patch 层通过 <see cref="RefreshLobbyMembers"/> 主动触发。
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
@@ -151,7 +233,8 @@ public class ConnectionManager
|
||||
{
|
||||
if (peer.TryTakeHelloSend(currentTime, out _))
|
||||
{
|
||||
SendHello?.Invoke(peer.PeerSteamId);
|
||||
var hello = new Hello { version = Const.version, mode = LocalMode };
|
||||
SendHello?.Invoke(peer.PeerSteamId, hello.ToBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +297,7 @@ public class ConnectionManager
|
||||
if (_peers.ContainsKey(peerId))
|
||||
return;
|
||||
|
||||
var peer = new PeerConnection(peerId, _localSteamId, OnPeerKcpOutput);
|
||||
var peer = new PeerConnection(peerId, _localSteamId, OnPeerKcpOutput, Time.time);
|
||||
_peers[peerId] = peer;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using Steamworks;
|
||||
using stick.plugins.kcpable.Protocol;
|
||||
using stick.plugins.kcpable.Transport.kcp;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.kcpable.Transport;
|
||||
|
||||
@@ -29,19 +28,35 @@ public class PeerConnection
|
||||
// KCP 实例(延迟初始化,key: channel)
|
||||
public Dictionary<int, Kcp> KcpChannels { get; } = new();
|
||||
|
||||
// 按 channel 的 conv 生成器
|
||||
// 按 channel 的 conv 生成器(懒创建)
|
||||
private readonly Dictionary<int, ConvGenerator> _convGens = new();
|
||||
|
||||
// KCP 数据输出回调:peerId, channel, buffer, size
|
||||
private readonly Action<CSteamID, int, byte[], int> _kcpOutput;
|
||||
|
||||
public PeerConnection(CSteamID peerSteamId, CSteamID localSteamId, Action<CSteamID, int, byte[], int> kcpOutput)
|
||||
private readonly CSteamID _localSteamId;
|
||||
|
||||
/// <summary>
|
||||
/// 本地配置的 KcpMode(由 ConnectionManager 在 MarkConnected 时注入)。
|
||||
/// </summary>
|
||||
public KcpMode LocalMode { get; set; } = KcpMode.Conservative;
|
||||
|
||||
/// <summary>
|
||||
/// 对端通过 Hello 包声明的 KcpMode。
|
||||
/// </summary>
|
||||
public KcpMode RemoteMode { get; set; } = KcpMode.Conservative;
|
||||
|
||||
/// <summary>
|
||||
/// 该对端实际生效的 KcpMode = min(LocalMode, RemoteMode)。
|
||||
/// </summary>
|
||||
public KcpMode EffectiveMode => (KcpMode)Math.Min((byte)LocalMode, (byte)RemoteMode);
|
||||
|
||||
public PeerConnection(CSteamID peerSteamId, CSteamID localSteamId, Action<CSteamID, int, byte[], int> kcpOutput, float startTime)
|
||||
{
|
||||
PeerSteamId = peerSteamId;
|
||||
_localSteamId = localSteamId;
|
||||
_kcpOutput = kcpOutput;
|
||||
_convGens[0] = new ConvGenerator(localSteamId, peerSteamId, 0);
|
||||
_convGens[1] = new ConvGenerator(localSteamId, peerSteamId, 1);
|
||||
HelloPhaseStartTime = Time.time;
|
||||
HelloPhaseStartTime = startTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,7 +98,6 @@ public class PeerConnection
|
||||
return;
|
||||
|
||||
State = PeerState.Connected;
|
||||
EnsureKcpCreated();
|
||||
}
|
||||
|
||||
public void MarkIdle()
|
||||
@@ -92,28 +106,35 @@ public class PeerConnection
|
||||
return;
|
||||
|
||||
State = PeerState.Idle;
|
||||
DisposeKcpChannels();
|
||||
ClearKcpChannels();
|
||||
}
|
||||
|
||||
private void EnsureKcpCreated()
|
||||
/// <summary>
|
||||
/// 懒创建指定 channel 的 KCP 实例。若已存在则直接返回。
|
||||
/// ConvGenerator 同样懒创建。
|
||||
/// </summary>
|
||||
public Kcp EnsureKcpForChannel(int channel)
|
||||
{
|
||||
if (KcpChannels.Count > 0)
|
||||
return;
|
||||
if (KcpChannels.TryGetValue(channel, out var existing))
|
||||
return existing;
|
||||
|
||||
foreach (int channel in new[] { 0, 1 })
|
||||
if (!_convGens.TryGetValue(channel, out var convGen))
|
||||
{
|
||||
uint conv = _convGens[channel].Generate();
|
||||
convGen = new ConvGenerator(_localSteamId, PeerSteamId, channel);
|
||||
_convGens[channel] = convGen;
|
||||
}
|
||||
|
||||
uint conv = convGen.Generate();
|
||||
int capturedChannel = channel;
|
||||
var kcp = new Kcp(conv, (data, size) => _kcpOutput(PeerSteamId, capturedChannel, data, size));
|
||||
// Turbo 模式:低延迟、快速重传、关闭拥塞窗口限制
|
||||
kcp.SetNoDelay(1, 10, 2, true);
|
||||
KcpChannels[channel] = kcp;
|
||||
}
|
||||
return kcp;
|
||||
}
|
||||
|
||||
private void DisposeKcpChannels()
|
||||
private void ClearKcpChannels()
|
||||
{
|
||||
// Kcp 本身无显式 Dispose,但释放字典引用即可让 GC 回收
|
||||
KcpChannels.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using Steamworks;
|
||||
|
||||
namespace stick.plugins.kcpable.Protocol;
|
||||
namespace stick.plugins.kcpable.Transport;
|
||||
|
||||
public class ConvGenerator(CSteamID local, CSteamID remote, int channel)
|
||||
{
|
||||
private readonly uint localId = local.GetAccountID().m_AccountID;
|
||||
private readonly uint remoteId = remote.GetAccountID().m_AccountID;
|
||||
private uint Smaller => Math.Min(localId, remoteId);
|
||||
private uint Bigger => Math.Max(localId, remoteId);
|
||||
private readonly uint smaller = Math.Min(local.GetAccountID().m_AccountID, remote.GetAccountID().m_AccountID);
|
||||
private readonly uint bigger = Math.Max(local.GetAccountID().m_AccountID, remote.GetAccountID().m_AccountID);
|
||||
private readonly uint channel = (uint)channel;
|
||||
public uint Generate() => Smaller ^ (Bigger << 1) ^ (channel << 2); // 位交错
|
||||
public uint Generate() => smaller ^ (bigger << 1) ^ (channel << 2); // 位交错
|
||||
}
|
||||
61
Transport/DecodedPacketBuffer.cs
Normal file
61
Transport/DecodedPacketBuffer.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
using Steamworks;
|
||||
|
||||
namespace stick.plugins.kcpable.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// KCP 解码后的包缓冲队列,按原始 channel 分组。
|
||||
/// Harmony Patch 层在 IsP2PPacketAvailable / ReadP2PPacket 拦截时使用。
|
||||
/// </summary>
|
||||
public class DecodedPacketBuffer
|
||||
{
|
||||
public static DecodedPacketBuffer Instance { get; } = new();
|
||||
|
||||
private readonly Dictionary<int, Queue<DecodedPacket>> _buffers = new();
|
||||
|
||||
public void Enqueue(int origChannel, CSteamID sender, byte[] data)
|
||||
{
|
||||
if (!_buffers.TryGetValue(origChannel, out var queue))
|
||||
{
|
||||
queue = new Queue<DecodedPacket>();
|
||||
_buffers[origChannel] = queue;
|
||||
}
|
||||
queue.Enqueue(new DecodedPacket { sender = sender, data = data });
|
||||
}
|
||||
|
||||
public bool TryDequeue(int origChannel, out CSteamID sender, out byte[] data)
|
||||
{
|
||||
if (_buffers.TryGetValue(origChannel, out var queue) && queue.Count > 0)
|
||||
{
|
||||
var pkt = queue.Dequeue();
|
||||
sender = pkt.sender;
|
||||
data = pkt.data;
|
||||
return true;
|
||||
}
|
||||
sender = default;
|
||||
data = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryPeekSize(int origChannel, out int size)
|
||||
{
|
||||
if (_buffers.TryGetValue(origChannel, out var queue) && queue.Count > 0)
|
||||
{
|
||||
size = queue.Peek().data.Length;
|
||||
return true;
|
||||
}
|
||||
size = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_buffers.Clear();
|
||||
}
|
||||
|
||||
private struct DecodedPacket
|
||||
{
|
||||
public CSteamID sender;
|
||||
public byte[] data;
|
||||
}
|
||||
}
|
||||
13
Transport/KcpMode.cs
Normal file
13
Transport/KcpMode.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace stick.plugins.kcpable.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// KCP 升级模式:
|
||||
/// Conservative = 仅 k_EP2PSendReliable 走 KCP,Unreliable 直通
|
||||
/// Aggressive = 全部 EP2PSend 走 KCP
|
||||
/// 对端模式取 min 自动降级,确保兼容。
|
||||
/// </summary>
|
||||
public enum KcpMode : byte
|
||||
{
|
||||
Conservative = 0,
|
||||
Aggressive = 1,
|
||||
}
|
||||
BIN
lib/TextMeshPro-1.0.55.56.0b9.dll
Normal file
BIN
lib/TextMeshPro-1.0.55.56.0b9.dll
Normal file
Binary file not shown.
359
plans/channel-offset-architecture.md
Normal file
359
plans/channel-offset-architecture.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Kcpable 架构变更:Channel Offset + 全 Channel 支持
|
||||
|
||||
## 1. 差距分析:当前 kcpable vs 游戏实际网络层
|
||||
|
||||
### 1.1 致命差距:只管理 Channel 0 和 1
|
||||
|
||||
| 项目 | 当前 kcpable | 游戏实际 |
|
||||
| -------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| 管理的 channel | 仅 `0`, `1` (硬编码在 [`EnsureKcpCreated()`](../Transport/ConnectionState.cs:103)) | `0,1` + 动态 `2~N` + `10,11` |
|
||||
| 发送 Hook 点 | 未实现 Patch 层 | 需 Hook [`SendP2PPacketToUser(CSteamID,...)`](../reference/P2PPackageHandler.cs:400) |
|
||||
| 接收 Hook 点 | 未实现 Patch 层 | 需覆盖 **三条独立读取路径** |
|
||||
| KCP 配置 | 全部 Turbo 模式 | 需分 Reliable/Unreliable 策略 |
|
||||
|
||||
### 1.2 三条接收路径都绕过 P2PPackageHandler
|
||||
|
||||
| 路径 | 读取位置 | 涉及 Channel |
|
||||
| ---- | -------------------------------------------------------------------------------------------- | ----------------- |
|
||||
| ① | [`P2PPackageHandler.CheckForPackagesOnChannel()`](../reference/P2PPackageHandler.cs:139) | 0, 1 |
|
||||
| ② | [`SyncableObjectManager.ListenForPackages()`](../reference/SyncableObjectManager.cs:27) | 10, 11 |
|
||||
| ③ | [`NetworkPlayer.ListenForPositionPackages()`](../reference/NetworkPlayer.cs:503) 等 4 个方法 | 2,3,4,5... (动态) |
|
||||
|
||||
三条路径都**直接调用** `SteamNetworking.IsP2PPacketAvailable()` + `ReadP2PPacket()`,完全绕过 `P2PPackageHandler`。
|
||||
|
||||
---
|
||||
|
||||
## 2. Channel Offset 方案:完全可行且是最佳方案
|
||||
|
||||
### 核心思路
|
||||
|
||||
```
|
||||
游戏读写 channel X (0~11+) ──→ Harmony Patch 拦截 ──→ Steam 实际读写 channel (X + 10000)
|
||||
│
|
||||
KCP 编解码 (基于原始 X) ←──────┘
|
||||
```
|
||||
|
||||
### 为什么这是最佳方案
|
||||
|
||||
1. **无需修改游戏任何代码**:游戏仍认为自己读写 channel 0~11
|
||||
2. **透明拦截所有三条接收路径**:在 `SteamNetworking` API 层统一 Hook,三条路径全部覆盖
|
||||
3. **发送端也自动覆盖**:`SendP2PPacketToUser(CSteamID,...)` 是唯一 Steam P2P 发送出口
|
||||
4. **不会与游戏自身 channel 冲突**:10000 远大于游戏使用的 0~11+
|
||||
|
||||
### 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph GameSend["游戏发送层"]
|
||||
SendToUser["SendP2PPacketToUser<br/>CSteamID + channel X"]
|
||||
end
|
||||
|
||||
subgraph HarmonyPatch["Harmony Patch 层"]
|
||||
SendPrefix["Prefix: 拦截发送"]
|
||||
RecvPrefix["Prefix: 拦截 IsP2PPacketAvailable<br/>+ ReadP2PPacket"]
|
||||
end
|
||||
|
||||
subgraph KcpCore["KCP 核心层 kcpable"]
|
||||
TrySend["ConnectionManager.TrySend<br/>key=remoteSteamID, origChannel"]
|
||||
KcpSession["KcpSession<br/>conv=fsteamID, channel"]
|
||||
DecodedBuf["解码包缓冲队列<br/>per origChannel"]
|
||||
end
|
||||
|
||||
subgraph SteamTransport["Steam P2P 传输层 全 Unreliable"]
|
||||
SteamSend["SendP2PPacket<br/>channel = X + 10000"]
|
||||
SteamRecv["IsP2PPacketAvailable<br/>+ ReadP2PPacket<br/>channel = X + 10000"]
|
||||
end
|
||||
|
||||
SendToUser -->|"① 拦截"| SendPrefix
|
||||
SendPrefix -->|"② 原始 channel X"| TrySend
|
||||
TrySend -->|"③ kcp.Send"| KcpSession
|
||||
KcpSession -->|"④ output 回调"| SteamSend
|
||||
|
||||
SteamRecv -->|"⑤ 读取 raw KCP 数据"| RecvPrefix
|
||||
RecvPrefix -->|"⑥ kcp.Input"| KcpSession
|
||||
KcpSession -->|"⑦ kcp.Recv 解码"| DecodedBuf
|
||||
DecodedBuf -->|"⑧ 返回给游戏调用者"| RecvPrefix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 需要变更的具体内容
|
||||
|
||||
### 变更 1:`PeerConnection` — 动态 Channel 懒创建
|
||||
|
||||
**当前代码** ([`ConnectionState.cs:98-112`](../Transport/ConnectionState.cs:98)):
|
||||
```csharp
|
||||
private void EnsureKcpCreated()
|
||||
{
|
||||
if (KcpChannels.Count > 0) return; // ← 问题:只要有一个 channel 就跳过
|
||||
foreach (int channel in new[] { 0, 1 }) // ← 问题:硬编码 0,1
|
||||
{
|
||||
// ... 创建 KCP 实例
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**需改为**:按需懒创建任意 channel 的 KCP 实例。
|
||||
|
||||
```csharp
|
||||
// 新增方法:确保特定 channel 的 KCP 实例存在
|
||||
public Kcp EnsureKcpForChannel(int channel)
|
||||
{
|
||||
if (!KcpChannels.TryGetValue(channel, out var kcp))
|
||||
{
|
||||
if (!_convGens.TryGetValue(channel, out var convGen))
|
||||
{
|
||||
convGen = new ConvGenerator(_localSteamId, PeerSteamId, channel);
|
||||
_convGens[channel] = convGen;
|
||||
}
|
||||
uint conv = convGen.Generate();
|
||||
int capturedChannel = channel;
|
||||
kcp = new Kcp(conv, (data, size) => _kcpOutput(PeerSteamId, capturedChannel, data, size));
|
||||
kcp.SetNoDelay(1, 10, 2, true); // 默认 Turbo
|
||||
KcpChannels[channel] = kcp;
|
||||
}
|
||||
return kcp;
|
||||
}
|
||||
```
|
||||
|
||||
同时 `ConnectionManager.TrySend()` 和 `OnKcpDataReceived()` 需改为调用 `EnsureKcpForChannel()` 而非检查 `TryGetValue` 后直接返回 false。
|
||||
|
||||
### 变更 2:添加 Channel Offset 常量
|
||||
|
||||
新文件或在 [`Const.cs`](../Protocol/Const.cs) 中添加:
|
||||
|
||||
```csharp
|
||||
// Protocol/Const.cs 追加
|
||||
public static class Const
|
||||
{
|
||||
public const byte version = 1;
|
||||
|
||||
/// <summary>
|
||||
/// KCP 传输层使用的 channel 偏移量。
|
||||
/// 游戏读写 channel X,KCP 在 Steam 层实际使用 channel X + OFFSET。
|
||||
/// 10000 远大于游戏最大 channel (约 2*maxPlayers+2),确保永不冲突。
|
||||
/// </summary>
|
||||
public const int KCP_CHANNEL_OFFSET = 10000;
|
||||
}
|
||||
```
|
||||
|
||||
### 变更 3:`ConnectionManager` 输出回调带 Offset
|
||||
|
||||
当前 `OnPeerKcpOutput` 直接透传 channel:
|
||||
```csharp
|
||||
private void OnPeerKcpOutput(CSteamID peerId, int channel, byte[] data, int size)
|
||||
{
|
||||
OnKcpOutput?.Invoke(peerId, channel, data, size); // channel = 原始 channel
|
||||
}
|
||||
```
|
||||
|
||||
Patch 层在 `OnKcpOutput` 回调中应使用 `channel + KCP_CHANNEL_OFFSET` 调用 `SteamNetworking.SendP2PPacket()`。
|
||||
|
||||
> **设计决策**:offset 放在 Patch 层而非 `ConnectionManager` 内部,保持 `ConnectionManager` 只关心**原始** channel,职责单一。
|
||||
|
||||
### 变更 4:接收侧架构 — 解码包缓冲队列 + Steam API Hook
|
||||
|
||||
需要新增一个**解码包缓冲**结构:
|
||||
|
||||
```csharp
|
||||
// Transport/DecodedPacketBuffer.cs (新文件)
|
||||
public class DecodedPacketBuffer
|
||||
{
|
||||
// 按原始 channel 分组的已解码包队列
|
||||
// key = originalChannel, value = 等待被游戏消费的包队列
|
||||
private readonly Dictionary<int, Queue<DecodedPacket>> _buffers = new();
|
||||
|
||||
public void Enqueue(int channel, CSteamID sender, byte[] data);
|
||||
public bool TryDequeue(int channel, out CSteamID sender, out byte[] data, out int size);
|
||||
public bool HasPending(int channel);
|
||||
}
|
||||
```
|
||||
|
||||
Patch 层需实现两个 Harmony Prefix:
|
||||
|
||||
#### Prefix on `IsP2PPacketAvailable`
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SteamNetworking), "IsP2PPacketAvailable",
|
||||
typeof(uint), typeof(int))]
|
||||
static bool IsP2PPacketAvailable_Prefix(ref bool __result, out uint msgSize, int channel)
|
||||
{
|
||||
// 1. 检查解码缓冲区是否有待消费数据
|
||||
if (DecodedPacketBuffer.Instance.TryPeek(channel, out int pendingSize))
|
||||
{
|
||||
msgSize = (uint)pendingSize;
|
||||
__result = true;
|
||||
return false; // 跳过原方法
|
||||
}
|
||||
|
||||
// 2. 从 Steam channel (channel + OFFSET) 读取原始 KCP 数据
|
||||
int kcpChannel = channel + Const.KCP_CHANNEL_OFFSET;
|
||||
if (SteamNetworking.IsP2PPacketAvailable(out uint rawSize, kcpChannel))
|
||||
{
|
||||
// 解码并放入缓冲区
|
||||
byte[] raw = new byte[rawSize];
|
||||
SteamNetworking.ReadP2PPacket(raw, rawSize, out _, out CSteamID sender, kcpChannel);
|
||||
ConnectionManager.Instance.OnKcpDataReceived(sender, channel, raw, 0, raw.Length);
|
||||
|
||||
// 再次检查解码缓冲区
|
||||
if (DecodedPacketBuffer.Instance.TryPeek(channel, out pendingSize))
|
||||
{
|
||||
msgSize = (uint)pendingSize;
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 回退到原始行为(允许游戏读原始 channel 的数据,用于兼容)
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### Prefix on `ReadP2PPacket`
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SteamNetworking), "ReadP2PPacket",
|
||||
typeof(byte[]), typeof(uint), typeof(uint), typeof(CSteamID), typeof(int))]
|
||||
static bool ReadP2PPacket_Prefix(byte[] dest, uint cubDest, out uint msgSize,
|
||||
out CSteamID sender, int channel)
|
||||
{
|
||||
if (DecodedPacketBuffer.Instance.TryDequeue(channel, out sender, out byte[] data, out int size))
|
||||
{
|
||||
Array.Copy(data, 0, dest, 0, size);
|
||||
msgSize = (uint)size;
|
||||
return false; // 返回 KCP 解码数据
|
||||
}
|
||||
// 回退到原始行为
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
> **为什么 Hook SteamNetworking 而非游戏层**:三条接收路径(P2PPackageHandler、SyncableObjectManager、NetworkPlayer)都直接调用 SteamNetworking API。Hook 这一层可一次性覆盖全部。
|
||||
|
||||
### 变更 5:发送侧 — Hook `SendP2PPacketToUser(CSteamID,...)`
|
||||
|
||||
这是游戏**唯一的 Steam P2P 发送出口**,Hook 它覆盖所有发送场景(含 `SendMessageToAllClients` 广播和 `SendP2PPacketToServer` 代理)。
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(P2PPackageHandler), "SendP2PPacketToUser",
|
||||
new Type[] { typeof(CSteamID), typeof(byte[]),
|
||||
typeof(P2PPackageHandler.MsgType), typeof(EP2PSend), typeof(int) })]
|
||||
static bool SendP2PPacketToUser_Prefix(P2PPackageHandler __instance,
|
||||
CSteamID clientID, byte[] data, P2PPackageHandler.MsgType messageType,
|
||||
EP2PSend sendMethod, int channel)
|
||||
{
|
||||
// 1. 打包消息(复用游戏原有的序列化)
|
||||
byte[] rawPacket = WriteMessageBuffer(__instance, data, messageType);
|
||||
|
||||
// 2. 尝试 KCP 发送
|
||||
if (ConnectionManager.Instance.TrySend(clientID, channel, rawPacket, rawPacket.Length))
|
||||
{
|
||||
return false; // KCP 已接管,跳过原 Steam 发送
|
||||
}
|
||||
|
||||
// 3. KCP 未就绪(比如握手未完成),回退原行为
|
||||
return true;
|
||||
}
|
||||
|
||||
// 需要反射或直接调用 private 方法
|
||||
private static byte[] WriteMessageBuffer(P2PPackageHandler instance,
|
||||
byte[] data, P2PPackageHandler.MsgType messageType)
|
||||
{
|
||||
// P2PPackageHandler.WriteMessageBuffer 是 private 方法
|
||||
// 可通过 Harmony AccessTools 或反射调用
|
||||
}
|
||||
```
|
||||
|
||||
### 变更 6:Reliable / Unreliable 分策略
|
||||
|
||||
| 原始发送方式 | Channel 示例 | KCP 策略 |
|
||||
| ----------------------------- | --------------- | ---------------------------------------------------------------- |
|
||||
| `k_EP2PSendReliable` | 0,1,3,5,7...,11 | KCP Turbo 模式 (`nodelay=1, interval=10, resend=2, nocwnd=true`) |
|
||||
| `k_EP2PSendUnreliableNoDelay` | 2,4,6...,10 | **直通**(不走 KCP),因为位置更新不应被 KCP 有序可靠传输阻塞 |
|
||||
|
||||
**关键问题**:KCP **始终保证有序可靠交付**。对于 Unreliable 位置更新,这会产生两个问题:
|
||||
1. 丢失的旧位置包会阻塞后续新位置包(有序性)
|
||||
2. 重传到达时位置数据已陈旧
|
||||
|
||||
**决策**:第一阶段**只 KCP-ify Reliable channel**。Unreliable channel 直接走 Steam `k_EP2PSendUnreliableNoDelay`,不做任何拦截。
|
||||
|
||||
实现方式:`SendP2PPacketToUser_Prefix` 中检查 `sendMethod`:
|
||||
- `k_EP2PSendUnreliable` / `k_EP2PSendUnreliableNoDelay` → 跳过 KCP,执行原方法
|
||||
- `k_EP2PSendReliable` → 走 KCP
|
||||
|
||||
### 变更 7:`ConnectionManager` 接口调整
|
||||
|
||||
`TrySend` 和 `OnKcpDataReceived` 需支持动态 channel:
|
||||
|
||||
```csharp
|
||||
// TrySend — 改为懒创建 KCP
|
||||
public bool TrySend(CSteamID peerId, int channel, byte[] data, int length)
|
||||
{
|
||||
if (!_peers.TryGetValue(peerId, out var peer)) return false;
|
||||
if (peer.State != PeerState.Connected) return false;
|
||||
|
||||
var kcp = peer.EnsureKcpForChannel(channel); // 懒创建
|
||||
kcp.Send(data, 0, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
// OnKcpDataReceived — 同样懒创建
|
||||
public void OnKcpDataReceived(CSteamID peerId, int channel, byte[] data, int offset, int length)
|
||||
{
|
||||
if (!_peers.TryGetValue(peerId, out var peer)) return;
|
||||
if (peer.State != PeerState.Connected) return;
|
||||
|
||||
var kcp = peer.EnsureKcpForChannel(channel); // 懒创建
|
||||
kcp.Input(data, offset, length);
|
||||
|
||||
// 尝试从 KCP 解码并放入 DecodedPacketBuffer
|
||||
int recvSize = kcp.PeekSize();
|
||||
if (recvSize > 0)
|
||||
{
|
||||
byte[] buf = new byte[recvSize];
|
||||
kcp.Receive(buf, buf.Length);
|
||||
DecodedPacketBuffer.Instance.Enqueue(channel, peerId, buf);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 变更 8:`ConvGenerator` 验证
|
||||
|
||||
当前实现:
|
||||
```csharp
|
||||
public uint Generate() => Smaller ^ (Bigger << 1) ^ (channel << 2);
|
||||
```
|
||||
|
||||
- `channel` 最大约 `2*maxPlayers + 2`(比如 8 人 → channel 最大 18)
|
||||
- `channel << 2` = 72,远小于 `uint.MaxValue`
|
||||
- XOR 位交错方案不会溢出,**当前实现已足够,无需修改**
|
||||
|
||||
---
|
||||
|
||||
## 4. 需要新增的文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ---------------------------------- | --------------------------------------------------------------- |
|
||||
| `Transport/DecodedPacketBuffer.cs` | 接收侧 KCP 解码后的包缓冲队列,按 originalChannel 分组 |
|
||||
| `Patches/` 目录 | Harmony Patch 类(发送/接收 Hook),与 `Plugin.cs` 同层或子目录 |
|
||||
|
||||
## 5. 需要修改的现有文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`Protocol/Const.cs`](../Protocol/Const.cs) | 添加 `KCP_CHANNEL_OFFSET = 10000` |
|
||||
| [`Transport/ConnectionState.cs`](../Transport/ConnectionState.cs) | `EnsureKcpCreated()` → 改为 `EnsureKcpForChannel(int channel)` 懒创建;`PeerConnection` 构造函数需存储 `_localSteamId` |
|
||||
| [`Transport/ConnectionManager.cs`](../Transport/ConnectionManager.cs) | `TrySend()` 和 `OnKcpDataReceived()` 改用懒创建;`OnKcpDataReceived()` 新增解码后入队逻辑 |
|
||||
| [`Plugin.cs`](../Plugin.cs) | 注册 Harmony Patch,初始化 `DecodedPacketBuffer` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 不在此阶段处理的事项
|
||||
|
||||
- Unreliable channel 的 KCP 化(需要 KCP 的非可靠模式或独立配置)
|
||||
- 协议降级(从 Connected 退回 Idle/TCP)
|
||||
- 重连恢复
|
||||
- 带宽统计与调试 UI
|
||||
303
plans/final-architecture.md
Normal file
303
plans/final-architecture.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Kcpable 最终架构设计
|
||||
|
||||
## 1. 核心决策汇总
|
||||
|
||||
| 决策项 | 结论 | 理由 |
|
||||
| -------------- | ------------------------------- | ----------------------------------------------- |
|
||||
| Channel Offset | `channel + 10000` | 游戏最大 channel ≈ 2×players+2,10000 永不冲突 |
|
||||
| 升级策略 | 双模式,配置化 | Conservative 稳,Aggressive 可测试 |
|
||||
| 模式协商 | Hello 包声明,取 `min` 降级 | 不握手,不增加往返,自动兼容 |
|
||||
| 接收 Hook | `SteamNetworking` API 层 | 一次性覆盖全部 3 条接收路径 |
|
||||
| 发送 Hook | `SendP2PPacketToUser(CSteamID)` | 游戏唯一 Steam P2P 发送出口 |
|
||||
| KCP 配置 | Turbo 模式统一 | `nodelay=1, interval=10, resend=2, nocwnd=true` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 双模式定义
|
||||
|
||||
```csharp
|
||||
public enum KcpMode : byte
|
||||
{
|
||||
Conservative = 0, // 仅 k_EP2PSendReliable 走 KCP
|
||||
Aggressive = 1, // 全部 EP2PSend 走 KCP
|
||||
}
|
||||
```
|
||||
|
||||
### 模式效果矩阵
|
||||
|
||||
| 原始 sendMethod | Conservative | Aggressive |
|
||||
| --------------------------------- | ------------ | ---------- |
|
||||
| `k_EP2PSendReliable` | ✅ KCP | ✅ KCP |
|
||||
| `k_EP2PSendReliableWithBuffering` | ✅ KCP | ✅ KCP |
|
||||
| `k_EP2PSendUnreliable` | ❌ Steam 直通 | ✅ KCP |
|
||||
| `k_EP2PSendUnreliableNoDelay` | ❌ Steam 直通 | ✅ KCP |
|
||||
|
||||
### 有效模式计算(Per-Peer)
|
||||
|
||||
```
|
||||
effectiveMode = min(localConfigMode, remoteHelloMode)
|
||||
```
|
||||
|
||||
| 本地配置 | 对端声明 | 生效模式 |
|
||||
| ------------ | ------------ | --------------------------- |
|
||||
| Conservative | Conservative | Conservative |
|
||||
| Conservative | Aggressive | **Conservative** (本地限制) |
|
||||
| Aggressive | Conservative | **Conservative** (对端限制) |
|
||||
| Aggressive | Aggressive | Aggressive |
|
||||
|
||||
> **关键安全性**:只要有一方是 Conservative,最终就是 Conservative。Aggressive 必须双方都是 Aggressive 才会启用。
|
||||
|
||||
---
|
||||
|
||||
## 3. Hello 包格式变更
|
||||
|
||||
```
|
||||
旧格式: [version:1 byte]
|
||||
新格式: [version:1 byte] [mode:1 byte]
|
||||
```
|
||||
|
||||
| Byte | 字段 | 值 |
|
||||
| ---- | --------- | ------------------------------------ |
|
||||
| 0 | `version` | `1` (Const.version) |
|
||||
| 1 | `mode` | `0` = Conservative, `1` = Aggressive |
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 旧版 kcpable(只发 1 字节):对端收到后 `mode` 读取越界 → 默认视为 Conservative。**安全降级。**
|
||||
- 原版游戏(无 kcpable):Hello 包的 MsgType=75 被忽略。**透明。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Config["BepInEx Config"]
|
||||
CfgMode["KcpMode: Conservative | Aggressive"]
|
||||
end
|
||||
|
||||
subgraph GameSend["游戏发送"]
|
||||
SendSteam["SendP2PPacketToUser<br/>CSteamID + EP2PSend + channel"]
|
||||
end
|
||||
|
||||
subgraph HarmonySend["Harmony Prefix: 发送拦截"]
|
||||
CheckMode{"effectiveMode<br/>= min(local, remote)"}
|
||||
CheckSend{"sendMethod?"}
|
||||
end
|
||||
|
||||
subgraph KcpSend["KCP 发送"]
|
||||
TrySend["ConnectionManager.TrySend<br/>key=peerId, origChannel"]
|
||||
KcpOut["KCP output → Steam<br/>channel + 10000, Unreliable"]
|
||||
end
|
||||
|
||||
subgraph SteamDirect["Steam 直通"]
|
||||
OrigSend["SendP2PPacket<br/>原 channel, 原 sendMethod"]
|
||||
end
|
||||
|
||||
SendSteam --> CheckMode
|
||||
CheckMode -->|"Aggressive"| TrySend
|
||||
CheckMode -->|"Conservative"| CheckSend
|
||||
CheckSend -->|"Reliable"| TrySend
|
||||
CheckSend -->|"Unreliable*"| OrigSend
|
||||
|
||||
subgraph GameRecv["游戏接收 三条路径"]
|
||||
P2PHandler["P2PPackageHandler<br/>ch 0,1"]
|
||||
SyncObj["SyncableObjectManager<br/>ch 10,11"]
|
||||
NetPlayer["NetworkPlayer<br/>ch 2,3,4..."]
|
||||
end
|
||||
|
||||
subgraph HarmonyRecv["Harmony Prefix: 接收拦截"]
|
||||
IsAvail["IsP2PPacketAvailable<br/>channel X → read X+10000"]
|
||||
ReadPkt["ReadP2PPacket<br/>返回 KCP 解码数据"]
|
||||
DecodeBuf["DecodedPacketBuffer<br/>per origChannel"]
|
||||
end
|
||||
|
||||
subgraph KcpRecv["KCP 接收"]
|
||||
KcpInput["OnKcpDataReceived<br/>kcp.Input + kcp.Recv"]
|
||||
end
|
||||
|
||||
P2PHandler --> IsAvail
|
||||
SyncObj --> IsAvail
|
||||
NetPlayer --> IsAvail
|
||||
IsAvail --> KcpInput
|
||||
KcpInput --> DecodeBuf
|
||||
DecodeBuf --> ReadPkt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 需要变更的文件清单
|
||||
|
||||
### 5.1 修改现有文件
|
||||
|
||||
| 文件 | 变更内容 |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| [`Protocol/Const.cs`](../Protocol/Const.cs) | 添加 `KCP_CHANNEL_OFFSET = 10000`,添加 `KcpMode` 枚举 |
|
||||
| [`Protocol/Hello.cs`](../Protocol/Hello.cs) | 发送时追加 `mode` 字节;新增 `FromBytes` 解析静态方法 |
|
||||
| [`Protocol/MsgType.cs`](../Protocol/MsgType.cs) | 无需变更(Hello 仍用 75) |
|
||||
| [`Transport/ConnectionState.cs`](../Transport/ConnectionState.cs) | `EnsureKcpCreated()` → `EnsureKcpForChannel(int channel)` 懒创建;存储 `_localSteamId`;存储 `_remoteMode` |
|
||||
| [`Transport/ConnectionManager.cs`](../Transport/ConnectionManager.cs) | `TrySend` 改懒创建;`OnKcpDataReceived` 改懒创建+解码入队;`OnHelloReceived` 解析 mode;新增 `GetEffectiveMode()` |
|
||||
| [`Plugin.cs`](../Plugin.cs) | 添加 BepInEx Config entry;注册 Harmony Patch;初始化 DecodedPacketBuffer |
|
||||
|
||||
### 5.2 新增文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --------------------------------------- | ---------------------------------------------------------- |
|
||||
| `Transport/DecodedPacketBuffer.cs` | 按 originalChannel 分组的解码包缓冲队列 |
|
||||
| `Transport/KcpMode.cs` | `KcpMode` 枚举 + `EffectiveMode` 计算 |
|
||||
| `Patches/Patch_SendP2PPacket.cs` | Harmony Prefix on `SendP2PPacketToUser(CSteamID,...)` |
|
||||
| `Patches/Patch_SteamNetworking_Recv.cs` | Harmony Prefix on `IsP2PPacketAvailable` + `ReadP2PPacket` |
|
||||
| `Patches/Patch_LobbyEvents.cs` | Harmony Patch 大厅进入/离开/成员变更事件 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 核心接口设计
|
||||
|
||||
### 6.1 ConnectionManager 新增/变更
|
||||
|
||||
```csharp
|
||||
public class ConnectionManager
|
||||
{
|
||||
// 新增:本地配置模式
|
||||
public KcpMode LocalMode { get; set; } = KcpMode.Conservative;
|
||||
|
||||
// 变更:OnHelloReceived 解析 mode
|
||||
public void OnHelloReceived(CSteamID fromPeer, KcpMode remoteMode);
|
||||
|
||||
// 新增:查询某对端的有效模式
|
||||
public KcpMode GetEffectiveMode(CSteamID peerId);
|
||||
|
||||
// 变更:TrySend 内部使用 EnsureKcpForChannel
|
||||
public bool TrySend(CSteamID peerId, int channel, byte[] data, int length);
|
||||
|
||||
// 变更:OnKcpDataReceived 解码后自动入队 DecodedPacketBuffer
|
||||
public void OnKcpDataReceived(CSteamID peerId, int channel, byte[] data, int offset, int length);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 PeerConnection 变更
|
||||
|
||||
```csharp
|
||||
public class PeerConnection
|
||||
{
|
||||
// 新增:对端声明的模式
|
||||
public KcpMode RemoteMode { get; private set; }
|
||||
|
||||
// 新增:有效模式
|
||||
public KcpMode EffectiveMode =>
|
||||
(KcpMode)Math.Min((byte)LocalMode, (byte)RemoteMode);
|
||||
|
||||
// 变更:懒创建
|
||||
public Kcp EnsureKcpForChannel(int channel);
|
||||
|
||||
// 删除:EnsureKcpCreated() 和硬编码的 new[] { 0, 1 }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 DecodedPacketBuffer
|
||||
|
||||
```csharp
|
||||
public class DecodedPacketBuffer
|
||||
{
|
||||
private readonly Dictionary<int, Queue<DecodedPacket>> _buffers = new();
|
||||
|
||||
public void Enqueue(int origChannel, CSteamID sender, byte[] data);
|
||||
public bool TryDequeue(int origChannel, out CSteamID sender, out byte[] data);
|
||||
public bool TryPeekSize(int origChannel, out int size);
|
||||
public bool HasPending(int origChannel);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Harmony Patch 伪代码
|
||||
|
||||
#### 发送端
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(P2PPackageHandler), "SendP2PPacketToUser",
|
||||
new Type[] { typeof(CSteamID), typeof(byte[]), typeof(MsgType), typeof(EP2PSend), typeof(int) })]
|
||||
static bool Prefix(P2PPackageHandler __instance, CSteamID clientID,
|
||||
byte[] data, MsgType messageType, EP2PSend sendMethod, int channel)
|
||||
{
|
||||
var mgr = ConnectionManager.Instance;
|
||||
var effectiveMode = mgr.GetEffectiveMode(clientID);
|
||||
|
||||
// Conservative 模式下 Unreliable 直通
|
||||
if (effectiveMode == KcpMode.Conservative)
|
||||
{
|
||||
if (sendMethod == EP2PSend.k_EP2PSendUnreliable ||
|
||||
sendMethod == EP2PSend.k_EP2PSendUnreliableNoDelay)
|
||||
return true; // Steam 直通
|
||||
}
|
||||
|
||||
// 进入 KCP
|
||||
byte[] rawPacket = WriteMessageBuffer(__instance, data, messageType);
|
||||
if (mgr.TrySend(clientID, channel, rawPacket, rawPacket.Length))
|
||||
return false; // KCP 已接管
|
||||
|
||||
return true; // KCP 未就绪,回退
|
||||
}
|
||||
```
|
||||
|
||||
#### 接收端
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SteamNetworking), "IsP2PPacketAvailable",
|
||||
typeof(uint), typeof(int))]
|
||||
static bool IsP2PPacketAvailable_Prefix(ref bool __result, out uint msgSize, int channel)
|
||||
{
|
||||
// 1. 先检查 KCP 解码缓冲区
|
||||
if (DecodedPacketBuffer.Instance.TryPeekSize(channel, out int size))
|
||||
{
|
||||
msgSize = (uint)size;
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 从 Steam offset channel 读取 raw KCP 数据
|
||||
int kcpChannel = channel + Const.KCP_CHANNEL_OFFSET;
|
||||
while (SteamNetworking.IsP2PPacketAvailable(out uint rawSize, kcpChannel))
|
||||
{
|
||||
byte[] raw = new byte[rawSize];
|
||||
SteamNetworking.ReadP2PPacket(raw, rawSize, out _, out CSteamID sender, kcpChannel);
|
||||
ConnectionManager.Instance.OnKcpDataReceived(sender, channel, raw, 0, raw.Length);
|
||||
}
|
||||
|
||||
// 3. 再检查一次
|
||||
if (DecodedPacketBuffer.Instance.TryPeekSize(channel, out size))
|
||||
{
|
||||
msgSize = (uint)size;
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 回退(让游戏也检查原始 channel,兼容无 KCP 场景)
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. BepInEx Config
|
||||
|
||||
```ini
|
||||
[Kcpable]
|
||||
## KCP mode: Conservative = only upgrade Reliable channels;
|
||||
## Aggressive = upgrade ALL channels (may cause micro-stutters on packet loss)
|
||||
# Setting type: Conservative / Aggressive
|
||||
# Default value: Conservative
|
||||
KcpMode = Conservative
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 阶段规划
|
||||
|
||||
| Phase | 内容 | 状态 |
|
||||
| ------- | ------------------------------------------- | ------ |
|
||||
| Phase 0 | 本架构文档 + 设计审查 | ✅ 当前 |
|
||||
| Phase 1 | 动态 Channel + Channel Offset + 基础设施 | 待实现 |
|
||||
| Phase 2 | Conservative 模式:Reliable → KCP | 待实现 |
|
||||
| Phase 3 | Aggressive 模式 + Hello 模式声明 + 自动降级 | 待实现 |
|
||||
| Phase 4 | 真实网络测试 + 调优 | 待测试 |
|
||||
209
plans/protocol-comparison.md
Normal file
209
plans/protocol-comparison.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# KCP 升级策略:Reliable vs Unreliable 协议对比分析
|
||||
|
||||
## 1. 游戏实际使用情况(来自完整反编译源码)
|
||||
|
||||
### 全量统计
|
||||
|
||||
| 发送方式 | 消息类型 | 所在 Channel | 数量占比 |
|
||||
| ----------------------------- | ------------------------------ | ------------------ | -------- |
|
||||
| `k_EP2PSendUnreliableNoDelay` | `PlayerUpdate`, `ObjectUpdate` | 动态 2,4,6... ; 10 | **2 种** |
|
||||
| `k_EP2PSendReliable` | 其余全部 (~30+ 种) | 0,1,3,5,7...,11 | **~95%** |
|
||||
|
||||
### UnreliableNoDelay 仅有的两个调用点
|
||||
|
||||
```csharp
|
||||
// MultiplayerManager.cs:1745 — 玩家位置/武器状态同步
|
||||
this.SendMessageToAllClients(data, MsgType.PlayerUpdate, false, num,
|
||||
EP2PSend.k_EP2PSendUnreliableNoDelay, channel); // channel = 2,4,6...
|
||||
|
||||
// MultiplayerManager.cs:1758 — 可同步对象位置更新
|
||||
this.SendMessageToAllClients(data, MsgType.ObjectUpdate, true, 0UL,
|
||||
EP2PSend.k_EP2PSendUnreliableNoDelay, channel); // channel = 10
|
||||
```
|
||||
|
||||
**其他所有消息(PlayerTookDamage, PlayerFallOut, WeaponSpawned, MapChange...)全部走 `k_EP2PSendReliable`。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 三种协议的本质对比
|
||||
|
||||
> 注意:Steam "Reliable" 并非 TCP,而是 Steam 自有的基于 UDP 的可靠传输协议。与 KCP 是同类型的东西。
|
||||
|
||||
| 特性 | Steam Reliable | Steam UnreliableNoDelay | KCP Turbo (nodelay=1,10,2,true) |
|
||||
| -------------- | ------------------------ | ----------------------- | ------------------------------- |
|
||||
| **重传** | ✅ 超时重传 (RTO ~200ms+) | ❌ 丢弃 | ✅ 快速重传 (2 ACK span) |
|
||||
| **有序交付** | ✅ 严格有序 | ❌ 无序 | ✅ 严格有序 |
|
||||
| **Nagle 缓冲** | ✅ 有(等待合并小包) | ❌ 立即发送 | ❌ 10ms 间隔立即发 |
|
||||
| **拥塞控制** | ✅ 保守 | ❌ 无 | ❌ 无 (nocwnd=true) |
|
||||
| **最小 RTO** | ~200ms | N/A | **30ms** |
|
||||
| **重复包检测** | ✅ | ❌ | ✅ (基于 SN) |
|
||||
|
||||
### 关键差异可视化
|
||||
|
||||
```
|
||||
Steam Reliable 发送时间线:
|
||||
发包1 ──[Nagle等待~200ms]──→ 发包1+2合并 ──[丢包]──→ 等200ms RTO ──→ 重传
|
||||
|
||||
KCP Turbo 发送时间线:
|
||||
发包1 ──[10ms]──→ 发 ──[丢包]──→ 收2个后续ACK ──[~20ms]──→ 快速重传
|
||||
|
||||
Steam Unreliable 发送时间线:
|
||||
发包1 ──[立即]──→ 发 ──[丢包]──→ 丢弃,发下一个
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 两种升级策略在游戏场景中的对比
|
||||
|
||||
### 策略 A:KCP 覆盖 Reliable,Unreliable 保持原样
|
||||
|
||||
| | 升级前 | 升级后 | 预期效果 |
|
||||
| --------------------------- | ---------------- | --------- | ------------------------------------------- |
|
||||
| Channel 0,1 (控制/流程) | Steam Reliable | KCP Turbo | Nagle 消除 + 快重传 → 握手/地图加载更快 |
|
||||
| Channel 3,5,7... (玩家事件) | Steam Reliable | KCP Turbo | 伤害/跌落/弹射事件延迟从 ~200ms RTO → ~30ms |
|
||||
| Channel 11 (对象事件) | Steam Reliable | KCP Turbo | 对象销毁/生成事件更低延迟 |
|
||||
| Channel 2,4,6... (玩家位置) | Steam Unreliable | **不变** | 无影响 |
|
||||
| Channel 10 (对象位置) | Steam Unreliable | **不变** | 无影响 |
|
||||
|
||||
**收益**:约 95% 的消息量获得延迟改善
|
||||
**风险**:极低 — 语义不变(都是可靠有序),只是更换底层实现
|
||||
|
||||
### 策略 B:KCP 覆盖 Unreliable,Reliable 保持原样
|
||||
|
||||
| | 升级前 | 升级后 | 风险 |
|
||||
| --------------------------- | -------------- | ------------ | ---------------------------------- |
|
||||
| Channel 2,4,6... (玩家位置) | 无序不可靠 | **有序可靠** | ⚠️ 队头阻塞:丢失的旧位置阻断新位置 |
|
||||
| Channel 10 (对象位置) | 无序不可靠 | **有序可靠** | ⚠️ 同上 |
|
||||
| 其他全部 | Steam Reliable | **不变** | 无改善 |
|
||||
|
||||
**收益**:只有约 5% 的消息量受影响(且效果存疑)
|
||||
**风险**:**高** — 位置更新的语义被破坏
|
||||
|
||||
### 策略 B 的具体风险分析
|
||||
|
||||
```
|
||||
场景:玩家在快速移动,每秒发送 60 个位置包
|
||||
|
||||
Steam UnreliableNoDelay(原始):
|
||||
Pkt1✓ Pkt2✗ Pkt3✓ Pkt4✓ Pkt5✗ Pkt6✓
|
||||
→ 接收侧收到 1,3,4,6,丢失的 2,5 用插值填充,平滑
|
||||
|
||||
KCP 可靠有序(策略 B):
|
||||
Pkt1✓ Pkt2✗ Pkt3(暂存) Pkt4(暂存) Pkt5(暂存)
|
||||
→ 等 Pkt2 重传... 30ms 后 Pkt2 到达
|
||||
→ 一次性释放 Pkt2,3,4,5 — 玩家突然跳到 Pkt5 位置!
|
||||
→ 而且 Pkt2,3,4 的位罝数据已经过时
|
||||
```
|
||||
|
||||
**结论**:对位置更新使用可靠有序传输,等于用"数据完整性"换"时效性",在快节奏物理格斗游戏中是负优化。
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐方案:策略 A + 分 Channel KCP 配置
|
||||
|
||||
### 整体策略
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ SendP2PPacketToUser Hook │
|
||||
│ 检查 sendMethod │
|
||||
└─────────────┬───────────────┘
|
||||
│
|
||||
┌───────────────────┴───────────────────┐
|
||||
│ │
|
||||
k_EP2PSendReliable k_EP2PSendUnreliable*
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ KCP Turbo │ │ Steam 直通 │
|
||||
│ channel+10000 │ │ 原始 channel │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 两种通道的 KCP 配置区分
|
||||
|
||||
虽然 KCP 只接管 Reliable 通道,但不同通道的流量特征不同,应使用不同配置:
|
||||
|
||||
| Channel 组 | 流量特征 | KCP 配置 | 理由 |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------- | ----------------------- |
|
||||
| **0** (控制) | 低频、关键、小包 | `nodelay=1, interval=10, resend=2, nocwnd=true` (Turbo) | 握手/加入必须快速 |
|
||||
| **1** (流程) | 中频、中等包 | `nodelay=1, interval=10, resend=2, nocwnd=true` (Turbo) | 武器/地图切换需要低延迟 |
|
||||
| **3,5,7...** (玩家事件) | 中频、伤害/力/聊天 | `nodelay=1, interval=10, resend=2, nocwnd=true` (Turbo) | 伤害反馈必须即时 |
|
||||
| **11** (对象事件) | 低频 | `nodelay=1, interval=10, resend=2, nocwnd=true` (Turbo) | 对象交互需要低延迟 |
|
||||
|
||||
> 当前所有通道统一使用 Turbo 模式即可,后续可根据实际测试数据微调。
|
||||
|
||||
---
|
||||
|
||||
## 5. 为什么不"全部走 KCP + 全降级为 Unreliable"
|
||||
|
||||
你之前提到的方案:**Steam 层全部改用 `Unreliable`,由 KCP 统一提供可靠性**。
|
||||
|
||||
这在理论上最优雅,但存在一个实际问题:
|
||||
|
||||
```
|
||||
PlayerUpdate 每秒 60 次,每次约 50-100 bytes
|
||||
|
||||
全部走 KCP 时:
|
||||
优点:丢失的位置包被 KCP 快速重传(30ms vs 完全丢失)
|
||||
缺点:KCP 的有序性意味着丢失的包会阻塞后续所有包
|
||||
|
||||
实际体验:
|
||||
60fps 位置更新下,丢包率 2% → 每秒约 1 个包丢失
|
||||
→ KCP 需要 ~30ms 重传
|
||||
→ 这 30ms 内所有后续位置包被阻塞
|
||||
→ 30ms 后一次性释放 2-3 个累积的位置包
|
||||
→ 玩家看到对手"冻结 30ms 然后瞬移"
|
||||
```
|
||||
|
||||
**对比原版 Unreliable**:丢包率 2%,每 50 个包丢 1 个,接收侧用上一个有效包插值,几乎无感知。
|
||||
|
||||
所以"全部走 KCP"在理论上是干净的,但在位置更新的场景下反而劣化体验。
|
||||
|
||||
### 如果未来想做"全部走 KCP"
|
||||
|
||||
需要 KCP 的 **非可靠非有序模式**(类似 UDP 直通),这需要对 KCP 本身做修改:
|
||||
- 去除有序性约束(rcv_buf 不做排序等待)
|
||||
- 去除重传(或极低 dead_link=1)
|
||||
- 保留 nodelay 的低延迟特性
|
||||
|
||||
这可以作为第二阶段优化方向,但不建议在第一阶段实施。
|
||||
|
||||
---
|
||||
|
||||
## 6. 决策结论
|
||||
|
||||
| 决策项 | 结论 |
|
||||
| ------------------------------- | ------------------------------------------------------------- |
|
||||
| Reliable 通道 (0,1,3,5,7...,11) | ✅ **走 KCP** — 语义兼容,延迟明显降低 |
|
||||
| Unreliable 通道 (2,4,6...,10) | ❌ **不走 KCP** — 语义冲突,会造成负优化 |
|
||||
| Steam 层发送方式 | KCP 通道用 `k_EP2PSendUnreliable`,非 KCP 通道保持原样 |
|
||||
| Channel Offset | 仅 KCP 通道使用 `channel + 10000` |
|
||||
| 后续优化方向 | 考虑给 PlayerUpdate/ObjectUpdate 加 KCP 非可靠模式(Phase 2) |
|
||||
|
||||
### 对应的 Hook 实现伪代码
|
||||
|
||||
```csharp
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(P2PPackageHandler), "SendP2PPacketToUser",
|
||||
new Type[] { typeof(CSteamID), typeof(byte[]),
|
||||
typeof(P2PPackageHandler.MsgType), typeof(EP2PSend), typeof(int) })]
|
||||
static bool SendP2PPacketToUser_Prefix(P2PPackageHandler __instance,
|
||||
CSteamID clientID, byte[] data, P2PPackageHandler.MsgType messageType,
|
||||
EP2PSend sendMethod, int channel)
|
||||
{
|
||||
// 只拦截 Reliable 发送,Unreliable 直通原方法
|
||||
if (sendMethod == EP2PSend.k_EP2PSendUnreliable ||
|
||||
sendMethod == EP2PSend.k_EP2PSendUnreliableNoDelay)
|
||||
return true; // 走原 Steam 方法
|
||||
|
||||
// Reliable → KCP
|
||||
byte[] rawPacket = WriteMessageBuffer(__instance, data, messageType);
|
||||
if (ConnectionManager.Instance.TrySend(clientID, channel, rawPacket, rawPacket.Length))
|
||||
return false; // KCP 已接管
|
||||
|
||||
return true; // KCP 未就绪,回退原 Steam Reliable
|
||||
}
|
||||
```
|
||||
|
||||
> **重要**:`k_EP2PSendReliableWithBuffering` 也应进入 KCP,因为 KCP 的 nodelay 模式本质就是"Reliable 但不 buffering",比 Steam 的可靠缓冲模式更快。
|
||||
1248
reference/NetworkPlayer.cs
Normal file
1248
reference/NetworkPlayer.cs
Normal file
File diff suppressed because it is too large
Load Diff
631
reference/P2PPackageHandler.cs
Normal file
631
reference/P2PPackageHandler.cs
Normal file
@@ -0,0 +1,631 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Lidgren.Network;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
// Token: 0x020000CC RID: 204
|
||||
public class P2PPackageHandler : MonoBehaviour
|
||||
{
|
||||
// Token: 0x170000B8 RID: 184
|
||||
// (get) Token: 0x0600061A RID: 1562 RVA: 0x0002ADCE File Offset: 0x000291CE
|
||||
public static P2PPackageHandler Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return P2PPackageHandler._instance;
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x0600061B RID: 1563 RVA: 0x0002ADD5 File Offset: 0x000291D5
|
||||
private void Awake()
|
||||
{
|
||||
if (P2PPackageHandler._instance != null)
|
||||
{
|
||||
global::UnityEngine.Object.Destroy(this);
|
||||
return;
|
||||
}
|
||||
P2PPackageHandler._instance = this;
|
||||
this.m_MsInSeconds = this.m_SimulatedMS / 1000f;
|
||||
}
|
||||
|
||||
// Token: 0x0600061C RID: 1564 RVA: 0x0002AE08 File Offset: 0x00029208
|
||||
public void Init()
|
||||
{
|
||||
this.mNetworkHandler = global::UnityEngine.Object.FindObjectOfType<MultiplayerManager>();
|
||||
if (this.mNetworkHandler != null)
|
||||
{
|
||||
this.mHasHandler = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x0600061D RID: 1565 RVA: 0x0002AE30 File Offset: 0x00029230
|
||||
private void OnEnable()
|
||||
{
|
||||
if (!SteamManager.Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.m_P2PSessionRequest = Callback<P2PSessionRequest_t>.Create(new Callback<P2PSessionRequest_t>.DispatchDelegate(this.OnP2PSessionRequest));
|
||||
this.m_P2PSessionConnectFail = Callback<P2PSessionConnectFail_t>.Create(new Callback<P2PSessionConnectFail_t>.DispatchDelegate(this.OnP2PSessionConnectFail));
|
||||
this.m_SocketStatusCallback_t = Callback<SocketStatusCallback_t>.Create(new Callback<SocketStatusCallback_t>.DispatchDelegate(this.OnSocketStatusCallback));
|
||||
}
|
||||
|
||||
// Token: 0x0600061E RID: 1566 RVA: 0x0002AE90 File Offset: 0x00029290
|
||||
private void Update()
|
||||
{
|
||||
if (!SteamManager.Initialized || this.mPauseTraffic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.m_MsInSeconds = this.m_SimulatedMS / 1000f;
|
||||
if (!MatchmakingHandler.Instance.IsInsideLobby || !this.mHasHandler)
|
||||
{
|
||||
this.CheckForPackagesOnChannelInMainMenu(0);
|
||||
this.CheckForPackagesOnChannelInMainMenu(1);
|
||||
return;
|
||||
}
|
||||
this.CheckForPackagesOnChannel(1, true);
|
||||
this.CheckForPackagesOnChannel(0, true);
|
||||
}
|
||||
|
||||
// Token: 0x0600061F RID: 1567 RVA: 0x0002AF00 File Offset: 0x00029300
|
||||
private void CheckForPackagesOnChannelInMainMenu(int channel = 0)
|
||||
{
|
||||
this.CheckForPackagesOnChannel(channel, false);
|
||||
}
|
||||
|
||||
// Token: 0x06000620 RID: 1568 RVA: 0x0002AF0C File Offset: 0x0002930C
|
||||
private void ReadSocketMessages()
|
||||
{
|
||||
NetIncomingMessage netIncomingMessage;
|
||||
while ((netIncomingMessage = MatchMakingHandlerSockets.Instance.ReadMessage()) != null)
|
||||
{
|
||||
NetIncomingMessageType messageType = netIncomingMessage.MessageType;
|
||||
if (messageType != NetIncomingMessageType.StatusChanged)
|
||||
{
|
||||
if (messageType != NetIncomingMessageType.Data)
|
||||
{
|
||||
if (messageType != NetIncomingMessageType.DiscoveryRequest)
|
||||
{
|
||||
if (messageType != NetIncomingMessageType.DiscoveryResponse)
|
||||
{
|
||||
if (messageType != NetIncomingMessageType.DebugMessage)
|
||||
{
|
||||
Debug.Log("unhandled message with type: " + netIncomingMessage.MessageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(netIncomingMessage.ReadString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(string.Concat(new object[]
|
||||
{
|
||||
"Found server at ",
|
||||
netIncomingMessage.SenderEndPoint,
|
||||
" name: ",
|
||||
netIncomingMessage.ReadString()
|
||||
}));
|
||||
MatchMakingHandlerSockets.Instance.JoinServerAt(netIncomingMessage.SenderEndPoint);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MatchMakingHandlerSockets.Instance.SendServerDiscoveryResponse(netIncomingMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] data = netIncomingMessage.Data;
|
||||
this.ReadMessageBuffer(data, new CSteamID(1UL));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (netIncomingMessage.SenderConnection.Status)
|
||||
{
|
||||
case NetConnectionStatus.Connected:
|
||||
MatchMakingHandlerSockets.Instance.OnConnectedMessageRecieved(netIncomingMessage);
|
||||
break;
|
||||
}
|
||||
Debug.Log("Status Changed: " + netIncomingMessage.SenderConnection.Status.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x06000621 RID: 1569 RVA: 0x0002B0A4 File Offset: 0x000294A4
|
||||
private void CheckForPackagesOnChannel(int channel = 0, bool checkHandler = true)
|
||||
{
|
||||
if (checkHandler && !this.mHasHandler)
|
||||
{
|
||||
Debug.LogError("Checking messages with no handler: Returning...");
|
||||
return;
|
||||
}
|
||||
if (this.mPauseTraffic)
|
||||
{
|
||||
Debug.Log("Traffic Is Paused, returning");
|
||||
return;
|
||||
}
|
||||
if (MatchmakingHandler.RunningOnSockets)
|
||||
{
|
||||
this.ReadSocketMessages();
|
||||
return;
|
||||
}
|
||||
uint num;
|
||||
while (SteamNetworking.IsP2PPacketAvailable(out num, channel))
|
||||
{
|
||||
byte[] array = new byte[num];
|
||||
uint num2;
|
||||
CSteamID csteamID;
|
||||
if (!SteamNetworking.ReadP2PPacket(array, num, out num2, out csteamID, channel))
|
||||
{
|
||||
Debug.Log("Failed to read P2P Package!");
|
||||
return;
|
||||
}
|
||||
this.ReadMessageBuffer(array, csteamID);
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x06000622 RID: 1570 RVA: 0x0002B138 File Offset: 0x00029538
|
||||
private void ReadMessageBuffer(byte[] rawData, CSteamID SteamIdRemote)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(rawData))
|
||||
{
|
||||
using (BinaryReader binaryReader = new BinaryReader(memoryStream))
|
||||
{
|
||||
uint lastTimeStamp = MultiplayerManager.LastTimeStamp;
|
||||
uint num = binaryReader.ReadUInt32();
|
||||
P2PPackageHandler.MsgType msgType = (P2PPackageHandler.MsgType)binaryReader.ReadByte();
|
||||
if (!this.mNetworkHandler.HasBeenInitializedFromServer && msgType != P2PPackageHandler.MsgType.ClientInit && msgType != P2PPackageHandler.MsgType.ClientAccepted)
|
||||
{
|
||||
Debug.Log("Stopping packet: " + msgType + " Has not been inited by server yet!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (num < lastTimeStamp)
|
||||
{
|
||||
Debug.LogWarning("Packet is obsolete!");
|
||||
}
|
||||
byte[] array = binaryReader.ReadBytes(rawData.Length - 1);
|
||||
this.CheckMessageType(array, msgType, SteamIdRemote);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x06000623 RID: 1571 RVA: 0x0002B214 File Offset: 0x00029614
|
||||
private void CheckMessageType(byte[] data, P2PPackageHandler.MsgType type, CSteamID steamIdRemote)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case P2PPackageHandler.MsgType.Ping:
|
||||
if (data.Length > 0)
|
||||
{
|
||||
this.SendP2PPacketToUser(steamIdRemote, data, P2PPackageHandler.MsgType.PingResponse, EP2PSend.k_EP2PSendReliable, 0);
|
||||
}
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.PingResponse:
|
||||
if (steamIdRemote == SteamUser.GetSteamID())
|
||||
{
|
||||
}
|
||||
PingHandler.PingMessageRecieved(steamIdRemote.m_SteamID, data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientJoined:
|
||||
this.mNetworkHandler.OnClientJoined(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingAccepting:
|
||||
this.SendP2PPacketToUser(steamIdRemote, new byte[0], P2PPackageHandler.MsgType.ClientAccepted, EP2PSend.k_EP2PSendReliable, 0);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientAccepted:
|
||||
this.mNetworkHandler.OnClientAcceptedByServer();
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientInit:
|
||||
this.mNetworkHandler.OnInitFromServer(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingIndex:
|
||||
this.mNetworkHandler.OnPlayerRequestingIndex(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingToSpawn:
|
||||
this.mNetworkHandler.OnPlayerRequestingToSpawn(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientSpawned:
|
||||
this.mNetworkHandler.OnPlayerSpawned(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientReadyUp:
|
||||
this.mNetworkHandler.OnClientReadyUp(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.MapChange:
|
||||
this.mNetworkHandler.OnMapChanged(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.WeaponSpawned:
|
||||
this.mNetworkHandler.OnWeaponSpawned(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientRequestWeaponDrop:
|
||||
this.mNetworkHandler.OnPlayerRequestingWeaponDrop(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.WeaponDropped:
|
||||
this.mNetworkHandler.OnWeaponDropped(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.WeaponWasPickedUp:
|
||||
this.mNetworkHandler.OnWeaponWasPickedUp(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingWeaponPickUp:
|
||||
this.mNetworkHandler.OnPlayerRequestingWeaponPickUp(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.ObjectSpawned:
|
||||
this.mNetworkHandler.OnObjectSpawned(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.GroundWeaponsInit:
|
||||
this.mNetworkHandler.OnGroundWeaponsInit(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.MapInfo:
|
||||
this.mNetworkHandler.OnMapInfoRecieved(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.MapInfoSync:
|
||||
this.mNetworkHandler.OnMapDataRecieved(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.WorkshopMapsLoaded:
|
||||
this.mNetworkHandler.OnNewWorkshopMapsRecieved(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.StartMatch:
|
||||
this.mNetworkHandler.OnMatchStart(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.OptionsChanged:
|
||||
OptionsHolder.NetworkOptionsChanged(data);
|
||||
return;
|
||||
case P2PPackageHandler.MsgType.KickPlayer:
|
||||
this.mNetworkHandler.OnKicked(data);
|
||||
return;
|
||||
}
|
||||
throw new Exception("Messagetype: " + type + " Is not setup!");
|
||||
}
|
||||
|
||||
// Token: 0x06000624 RID: 1572 RVA: 0x0002B494 File Offset: 0x00029894
|
||||
public void SendP2PPacketToServer(byte[] data, P2PPackageHandler.MsgType messageType, EP2PSend sendMethod = EP2PSend.k_EP2PSendReliable, int channel = -1)
|
||||
{
|
||||
CSteamID lobbyOwner = MatchmakingHandler.Instance.LobbyOwner;
|
||||
int num = ((channel != -1) ? channel : this.GetChannelForMsgType(messageType));
|
||||
this.SendP2PPacketToUser(lobbyOwner, data, messageType, sendMethod, num);
|
||||
}
|
||||
|
||||
// Token: 0x06000625 RID: 1573 RVA: 0x0002B4D0 File Offset: 0x000298D0
|
||||
private int GetChannelForMsgType(P2PPackageHandler.MsgType messageType)
|
||||
{
|
||||
switch (messageType)
|
||||
{
|
||||
case P2PPackageHandler.MsgType.Ping:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.PingResponse:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientJoined:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingAccepting:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ClientAccepted:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ClientInit:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingIndex:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingToSpawn:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientSpawned:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.ClientReadyUp:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.PlayerForceAddedAndBlock:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.PlayerFallOut:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.MapChange:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.WeaponSpawned:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ClientRequestWeaponDrop:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.WeaponDropped:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.WeaponWasPickedUp:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ClientRequestingWeaponPickUp:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ObjectSpawned:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ObjectSimpleDestruction:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.ObjectInvokeDestructionEvent:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.GroundWeaponsInit:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.MapInfo:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.MapInfoSync:
|
||||
return 0;
|
||||
case P2PPackageHandler.MsgType.WorkshopMapsLoaded:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.StartMatch:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.OptionsChanged:
|
||||
return 1;
|
||||
case P2PPackageHandler.MsgType.KickPlayer:
|
||||
return 1;
|
||||
}
|
||||
throw new Exception("Message Type is not setup: " + messageType.ToString());
|
||||
}
|
||||
|
||||
// Token: 0x06000626 RID: 1574 RVA: 0x0002B5D8 File Offset: 0x000299D8
|
||||
public void SendSocketP2PPacketToUser(NetConnection user, byte[] data, P2PPackageHandler.MsgType messageType, NetDeliveryMethod sendMethod = NetDeliveryMethod.ReliableOrdered, int channel = 0)
|
||||
{
|
||||
uint maxValue = uint.MaxValue;
|
||||
byte[] array = new byte[data.Length + 4 + 1];
|
||||
using (MemoryStream memoryStream = new MemoryStream(array))
|
||||
{
|
||||
using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream))
|
||||
{
|
||||
binaryWriter.Write(maxValue);
|
||||
binaryWriter.Write((byte)messageType);
|
||||
binaryWriter.Write(data);
|
||||
}
|
||||
}
|
||||
this.SendSocketMessage(user, array, channel);
|
||||
}
|
||||
|
||||
// Token: 0x06000627 RID: 1575 RVA: 0x0002B664 File Offset: 0x00029A64
|
||||
private byte[] WriteMessageBuffer(byte[] data, P2PPackageHandler.MsgType messageType)
|
||||
{
|
||||
uint serverRealTime = SteamUtils.GetServerRealTime();
|
||||
byte[] array = new byte[data.Length + 4 + 1];
|
||||
using (MemoryStream memoryStream = new MemoryStream(array))
|
||||
{
|
||||
using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream))
|
||||
{
|
||||
binaryWriter.Write(serverRealTime);
|
||||
binaryWriter.Write((byte)messageType);
|
||||
binaryWriter.Write(data);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// Token: 0x06000628 RID: 1576 RVA: 0x0002B6E8 File Offset: 0x00029AE8
|
||||
public void SendP2PPacketToUser(NetConnection user, byte[] data, P2PPackageHandler.MsgType messageType, EP2PSend sendMethod = EP2PSend.k_EP2PSendReliable, int channel = 0)
|
||||
{
|
||||
byte[] array = this.WriteMessageBuffer(data, messageType);
|
||||
this.SendSocketMessage(user, array, channel);
|
||||
}
|
||||
|
||||
// Token: 0x06000629 RID: 1577 RVA: 0x0002B708 File Offset: 0x00029B08
|
||||
public void SendP2PPacketToUser(CSteamID clientID, byte[] data, P2PPackageHandler.MsgType messageType, EP2PSend sendMethod = EP2PSend.k_EP2PSendReliable, int channel = 0)
|
||||
{
|
||||
byte[] array = this.WriteMessageBuffer(data, messageType);
|
||||
uint num = (uint)array.Length;
|
||||
if (!SteamNetworking.SendP2PPacket(clientID, array, num, sendMethod, channel))
|
||||
{
|
||||
Debug.Log("FAILED send package to User: " + clientID.m_SteamID);
|
||||
}
|
||||
else
|
||||
{
|
||||
P2PStatistics.BytesWasSent(num, (P2PPackageHandler.MsgType)array[4]);
|
||||
if (messageType == P2PPackageHandler.MsgType.Ping && data.Length > 0)
|
||||
{
|
||||
PingHandler.AddPingMessage(clientID.m_SteamID, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x0600062A RID: 1578 RVA: 0x0002B77A File Offset: 0x00029B7A
|
||||
private void SendSocketMessage(CSteamID user, byte[] data, int channel)
|
||||
{
|
||||
}
|
||||
|
||||
// Token: 0x0600062B RID: 1579 RVA: 0x0002B77C File Offset: 0x00029B7C
|
||||
private void SendSocketMessage(NetConnection user, byte[] data, int channel)
|
||||
{
|
||||
MatchMakingHandlerSockets.Instance.SendSocketMessage(user, data, channel);
|
||||
}
|
||||
|
||||
// Token: 0x0600062C RID: 1580 RVA: 0x0002B78B File Offset: 0x00029B8B
|
||||
public void ResumeNetworkTraffic()
|
||||
{
|
||||
this.mPauseTraffic = false;
|
||||
Debug.Log("Resumed Network Traffic On time: " + Time.time);
|
||||
}
|
||||
|
||||
// Token: 0x0600062D RID: 1581 RVA: 0x0002B7AD File Offset: 0x00029BAD
|
||||
public void PauseNetworkTraffic()
|
||||
{
|
||||
this.mPauseTraffic = true;
|
||||
Debug.Log("Paused Network Traffic On time: " + Time.time);
|
||||
}
|
||||
|
||||
// Token: 0x0600062E RID: 1582 RVA: 0x0002B7D0 File Offset: 0x00029BD0
|
||||
private void OnP2PSessionRequest(P2PSessionRequest_t pCallback)
|
||||
{
|
||||
Debug.Log(string.Concat(new object[] { "[", 1202, " - P2PSessionRequest] - ", pCallback.m_steamIDRemote }));
|
||||
if (!MatchmakingHandler.Instance.IsInsideLobby)
|
||||
{
|
||||
Debug.LogError("Got a P2P request when not in lobby, denying request!");
|
||||
return;
|
||||
}
|
||||
CSteamID steamIDRemote = pCallback.m_steamIDRemote;
|
||||
if (!MatchmakingHandler.Instance.IsUserInsideMyLobby(steamIDRemote))
|
||||
{
|
||||
Debug.LogError(string.Concat(new object[]
|
||||
{
|
||||
"Requestee user: ",
|
||||
steamIDRemote,
|
||||
" : ",
|
||||
SteamFriends.GetFriendPersonaName(steamIDRemote),
|
||||
" Wants to establish a p2p connection but is not in lobby, denying..."
|
||||
}));
|
||||
return;
|
||||
}
|
||||
bool flag = SteamNetworking.AcceptP2PSessionWithUser(steamIDRemote);
|
||||
MonoBehaviour.print(string.Concat(new object[] { "SteamNetworking.AcceptP2PSessionWithUser(", steamIDRemote, ") - ", flag }));
|
||||
if (SteamFriends.RequestUserInformation(steamIDRemote, false))
|
||||
{
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
this.SendP2PPacketToUser(steamIDRemote, new byte[0], P2PPackageHandler.MsgType.Ping, EP2PSend.k_EP2PSendReliable, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x0600062F RID: 1583 RVA: 0x0002B8E8 File Offset: 0x00029CE8
|
||||
private void OnPersoneStateChanged(PersonaStateChange_t pCallback)
|
||||
{
|
||||
Debug.Log("OnPersoneStateChanged: " + pCallback.m_nChangeFlags);
|
||||
int smallFriendAvatar = SteamFriends.GetSmallFriendAvatar(new CSteamID(pCallback.m_ulSteamID));
|
||||
if (smallFriendAvatar == 0)
|
||||
{
|
||||
Debug.LogError("No Image is set for remote user!");
|
||||
}
|
||||
int smallFriendAvatar2 = SteamFriends.GetSmallFriendAvatar(SteamUser.GetSteamID());
|
||||
if (smallFriendAvatar2 == 0)
|
||||
{
|
||||
Debug.LogError("No Image is set for user!");
|
||||
}
|
||||
uint num = 8192U;
|
||||
byte[] array = new byte[num];
|
||||
byte[] array2 = new byte[num];
|
||||
if (!SteamUtils.GetImageRGBA(smallFriendAvatar, array, array.Length))
|
||||
{
|
||||
Debug.LogError("Image was not stored correctly!");
|
||||
}
|
||||
if (!SteamUtils.GetImageRGBA(smallFriendAvatar2, array2, array.Length))
|
||||
{
|
||||
Debug.LogError("Image was not stored correctly!");
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x06000630 RID: 1584 RVA: 0x0002B997 File Offset: 0x00029D97
|
||||
private void OnSocketStatusCallback(SocketStatusCallback_t pCallback)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Token: 0x06000631 RID: 1585 RVA: 0x0002B9A0 File Offset: 0x00029DA0
|
||||
private void OnP2PSessionConnectFail(P2PSessionConnectFail_t pCallback)
|
||||
{
|
||||
string text = "P2p SessionConnectFail! ";
|
||||
EP2PSessionError eP2PSessionError = (EP2PSessionError)pCallback.m_eP2PSessionError;
|
||||
Debug.LogError(text + eP2PSessionError.ToString());
|
||||
if (pCallback.m_eP2PSessionError == 4 && !MatchmakingHandler.IsNetworkMatch)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Token: 0x04000515 RID: 1301
|
||||
protected Callback<P2PSessionRequest_t> m_P2PSessionRequest;
|
||||
|
||||
// Token: 0x04000516 RID: 1302
|
||||
protected Callback<P2PSessionConnectFail_t> m_P2PSessionConnectFail;
|
||||
|
||||
// Token: 0x04000517 RID: 1303
|
||||
protected Callback<SocketStatusCallback_t> m_SocketStatusCallback_t;
|
||||
|
||||
// Token: 0x04000518 RID: 1304
|
||||
private bool mHasHandler;
|
||||
|
||||
// Token: 0x04000519 RID: 1305
|
||||
private MultiplayerManager mNetworkHandler;
|
||||
|
||||
// Token: 0x0400051A RID: 1306
|
||||
[SerializeField]
|
||||
private uint m_SimulatedMS;
|
||||
|
||||
// Token: 0x0400051B RID: 1307
|
||||
private float m_MsInSeconds;
|
||||
|
||||
// Token: 0x0400051C RID: 1308
|
||||
private static P2PPackageHandler _instance;
|
||||
|
||||
// Token: 0x0400051D RID: 1309
|
||||
private bool mPauseTraffic;
|
||||
|
||||
// Token: 0x020000CD RID: 205
|
||||
public enum MsgType : byte
|
||||
{
|
||||
// Token: 0x0400051F RID: 1311
|
||||
Ping,
|
||||
// Token: 0x04000520 RID: 1312
|
||||
PingResponse,
|
||||
// Token: 0x04000521 RID: 1313
|
||||
ClientJoined,
|
||||
// Token: 0x04000522 RID: 1314
|
||||
ClientRequestingAccepting,
|
||||
// Token: 0x04000523 RID: 1315
|
||||
ClientAccepted,
|
||||
// Token: 0x04000524 RID: 1316
|
||||
ClientInit,
|
||||
// Token: 0x04000525 RID: 1317
|
||||
ClientRequestingIndex,
|
||||
// Token: 0x04000526 RID: 1318
|
||||
ClientRequestingToSpawn,
|
||||
// Token: 0x04000527 RID: 1319
|
||||
ClientSpawned,
|
||||
// Token: 0x04000528 RID: 1320
|
||||
ClientReadyUp,
|
||||
// Token: 0x04000529 RID: 1321
|
||||
PlayerUpdate,
|
||||
// Token: 0x0400052A RID: 1322
|
||||
PlayerTookDamage,
|
||||
// Token: 0x0400052B RID: 1323
|
||||
PlayerTalked,
|
||||
// Token: 0x0400052C RID: 1324
|
||||
PlayerForceAdded,
|
||||
// Token: 0x0400052D RID: 1325
|
||||
PlayerForceAddedAndBlock,
|
||||
// Token: 0x0400052E RID: 1326
|
||||
PlayerLavaForceAdded,
|
||||
// Token: 0x0400052F RID: 1327
|
||||
PlayerFallOut,
|
||||
// Token: 0x04000530 RID: 1328
|
||||
PlayerWonWithRicochet,
|
||||
// Token: 0x04000531 RID: 1329
|
||||
MapChange,
|
||||
// Token: 0x04000532 RID: 1330
|
||||
WeaponSpawned,
|
||||
// Token: 0x04000533 RID: 1331
|
||||
WeaponThrown,
|
||||
// Token: 0x04000534 RID: 1332
|
||||
RequestingWeaponThrow,
|
||||
// Token: 0x04000535 RID: 1333
|
||||
ClientRequestWeaponDrop,
|
||||
// Token: 0x04000536 RID: 1334
|
||||
WeaponDropped,
|
||||
// Token: 0x04000537 RID: 1335
|
||||
WeaponWasPickedUp,
|
||||
// Token: 0x04000538 RID: 1336
|
||||
ClientRequestingWeaponPickUp,
|
||||
// Token: 0x04000539 RID: 1337
|
||||
ObjectUpdate,
|
||||
// Token: 0x0400053A RID: 1338
|
||||
ObjectSpawned,
|
||||
// Token: 0x0400053B RID: 1339
|
||||
ObjectSimpleDestruction,
|
||||
// Token: 0x0400053C RID: 1340
|
||||
ObjectInvokeDestructionEvent,
|
||||
// Token: 0x0400053D RID: 1341
|
||||
ObjectDestructionCollision,
|
||||
// Token: 0x0400053E RID: 1342
|
||||
GroundWeaponsInit,
|
||||
// Token: 0x0400053F RID: 1343
|
||||
MapInfo,
|
||||
// Token: 0x04000540 RID: 1344
|
||||
MapInfoSync,
|
||||
// Token: 0x04000541 RID: 1345
|
||||
WorkshopMapsLoaded,
|
||||
// Token: 0x04000542 RID: 1346
|
||||
StartMatch,
|
||||
// Token: 0x04000543 RID: 1347
|
||||
ObjectHello,
|
||||
// Token: 0x04000544 RID: 1348
|
||||
OptionsChanged,
|
||||
// Token: 0x04000545 RID: 1349
|
||||
KickPlayer
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,15 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TextMeshPro-1.0.55.56.0b9">
|
||||
<HintPath>lib/TextMeshPro-1.0.55.56.0b9.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 排除 reference/ 目录:仅为参考源码,不应参与编译 -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="reference/**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user