diff --git a/ClassLibrary1/DebugTools/DebugMenu.cs b/ClassLibrary1/DebugTools/DebugMenu.cs index 6bca4584..a1aebaba 100644 --- a/ClassLibrary1/DebugTools/DebugMenu.cs +++ b/ClassLibrary1/DebugTools/DebugMenu.cs @@ -1,4 +1,4 @@ -using ONI_MP.Networking; +using ONI_MP.Networking; using ONI_MP.Networking.States; using Steamworks; using System; @@ -6,88 +6,279 @@ namespace ONI_MP.DebugTools { - public class DebugMenu : MonoBehaviour - { - private static DebugMenu _instance; - - private bool showMenu = false; - private Rect windowRect = new Rect(10, 10, 250, 300); // Position and size - private HierarchyViewer hierarchyViewer; - private DebugConsole debugConsole; - - private Vector2 scrollPosition = Vector2.zero; - - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] - public static void Init() - { - if (_instance != null) return; - - GameObject go = new GameObject("ONI_MP_DebugMenu"); - DontDestroyOnLoad(go); - _instance = go.AddComponent(); - } - - private void Awake() - { - hierarchyViewer = gameObject.AddComponent(); - //debugConsole = gameObject.AddComponent(); - } - - private void Update() - { - //if (Input.GetKeyDown(KeyCode.F2) && (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) - //{ - // showMenu = !showMenu; - //} - } - - private void OnGUI() - { - if (!showMenu) return; - - GUIStyle windowStyle = new GUIStyle(GUI.skin.window) { padding = new RectOffset(10, 10, 20, 20) }; - windowRect = GUI.ModalWindow(888, windowRect, DrawMenuContents, "DEBUG MENU", windowStyle); - } - - private void DrawMenuContents(int windowID) - { - scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.Width(windowRect.width - 20), GUILayout.Height(windowRect.height - 40)); - - if (GUILayout.Button("Toggle Hierarchy Viewer")) - hierarchyViewer.Toggle(); - - if (GUILayout.Button("Send Unready Packet")) + public class DebugMenu : MonoBehaviour + { + private static DebugMenu _instance; + + private bool showMenu = false; + private Rect windowRect = new Rect(10, 10, 300, 450); // 稍微加大一点 + private HierarchyViewer hierarchyViewer; + private DebugConsole debugConsole; + + private Vector2 scrollPosition = Vector2.zero; + + // 直连相关 + private bool showDirectConnect = false; + private string directConnectIP = ""; + private string directConnectPort = "11000"; + private string directConnectStatus = ""; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + public static void Init() + { + if (_instance != null) return; + + GameObject go = new GameObject("ONI_MP_DebugMenu"); + DontDestroyOnLoad(go); + _instance = go.AddComponent(); + } + + private void Awake() + { + hierarchyViewer = gameObject.AddComponent(); + //debugConsole = gameObject.AddComponent(); + } + + private void Update() + { + // Shift + F1 打开调试菜单 + if (Input.GetKeyDown(KeyCode.F1) && (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) + { + showMenu = !showMenu; + } + + // Shift + F2 快速打开直连窗口 + if (Input.GetKeyDown(KeyCode.F2) && (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) + { + showDirectConnect = !showDirectConnect; + } + } + + private void OnGUI() + { + // 绘制主调试菜单 + if (showMenu) + { + GUIStyle windowStyle = new GUIStyle(GUI.skin.window) { padding = new RectOffset(10, 10, 20, 20) }; + windowRect = GUI.ModalWindow(888, windowRect, DrawMenuContents, "DEBUG MENU", windowStyle); + } + + // 绘制直连窗口 + if (showDirectConnect) + { + DrawDirectConnectWindow(); + } + } + + private void DrawMenuContents(int windowID) + { + scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.Width(windowRect.width - 20), GUILayout.Height(windowRect.height - 40)); + + GUILayout.Label("== 工具 =="); + + if (GUILayout.Button("Toggle Hierarchy Viewer")) + hierarchyViewer.Toggle(); + + GUILayout.Space(10); + GUILayout.Label("== 准备状态 =="); + + if (GUILayout.Button("Send Unready Packet")) ReadyManager.SendReadyStatusPacket(ClientReadyState.Unready); if (GUILayout.Button("Send Ready Packet")) ReadyManager.SendReadyStatusPacket(ClientReadyState.Ready); - GUILayout.EndScrollView(); - - GUI.DragWindow(); - } - - private void DrawPlayerList() - { - GUILayout.Label("Players in Lobby:", UnityEngine.GUI.skin.label); - - var players = SteamLobby.GetAllLobbyMembers(); - if (players.Count == 0) - { - GUILayout.Label("", UnityEngine.GUI.skin.label); - } - else - { - foreach (CSteamID playerId in players) - { - var playerName = SteamFriends.GetFriendPersonaName(playerId); - string prefix = (MultiplayerSession.HostSteamID == playerId) ? "[HOST] " : ""; - GUILayout.Label($"{prefix}{playerName} ({playerId})", UnityEngine.GUI.skin.label); - } - } - } - - - } + GUILayout.Space(10); + GUILayout.Label("== 网络 =="); + + // 显示当前连接模式 + string modeText = DirectConnection.Mode == ConnectionMode.DirectIP ? "直连模式" : "Steam P2P"; + GUILayout.Label($"当前模式: {modeText}"); + + if (GUILayout.Button("打开直连窗口 (Shift+F2)")) + { + showDirectConnect = !showDirectConnect; + } + + GUILayout.Space(10); + GUILayout.Label("== 玩家列表 =="); + DrawPlayerList(); + + GUILayout.EndScrollView(); + + GUI.DragWindow(); + } + + #region 直连窗口 + + private Rect directConnectRect = new Rect(0, 0, 380, 350); + private bool directConnectRectInitialized = false; + + private void DrawDirectConnectWindow() + { + // 居中窗口 + if (!directConnectRectInitialized) + { + directConnectRect.x = (Screen.width - directConnectRect.width) / 2; + directConnectRect.y = (Screen.height - directConnectRect.height) / 2; + directConnectRectInitialized = true; + } + + GUIStyle windowStyle = new GUIStyle(GUI.skin.window) { padding = new RectOffset(15, 15, 25, 15) }; + directConnectRect = GUI.Window(889, directConnectRect, DrawDirectConnectContents, "直连模式 / Direct Connect", windowStyle); + } + + private void DrawDirectConnectContents(int windowID) + { + GUIStyle labelStyle = new GUIStyle(GUI.skin.label) { richText = true, fontSize = 13 }; + GUIStyle buttonStyle = new GUIStyle(GUI.skin.button) { fontSize = 13, fixedHeight = 32 }; + GUIStyle textFieldStyle = new GUIStyle(GUI.skin.textField) { fontSize = 13, fixedHeight = 24 }; + GUIStyle boxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 10, 10) }; + + // 状态信息 + GUILayout.BeginVertical(boxStyle); + GUILayout.Label($"连接状态: {DirectConnection.GetConnectionInfo()}", labelStyle); + GUILayout.Label($"本机 IP: {DirectConnection.GetLocalIPAddress()}", labelStyle); + GUILayout.Label($"模式: {(DirectConnection.Mode == ConnectionMode.DirectIP ? "直连" : "Steam P2P")}", labelStyle); + GUILayout.EndVertical(); + + GUILayout.Space(10); + + // 输入区域 + GUILayout.Label("主机 IP 地址:", labelStyle); + directConnectIP = GUILayout.TextField(directConnectIP, textFieldStyle); + + GUILayout.Space(5); + + GUILayout.Label("端口 (默认 11000):", labelStyle); + directConnectPort = GUILayout.TextField(directConnectPort, textFieldStyle); + + GUILayout.Space(15); + + // 按钮区域 + if (!DirectConnection.IsServerRunning && !DirectConnection.IsClientConnected) + { + // 创建房间 + if (GUILayout.Button("🎮 创建房间 (作为主机)", buttonStyle)) + { + int port = int.TryParse(directConnectPort, out int p) ? p : DirectConnection.DEFAULT_PORT; + if (DirectConnection.StartServer(port)) + { + directConnectStatus = $"✓ 房间已创建!\n告诉朋友连接: {DirectConnection.GetLocalIPAddress()}:{port}"; + } + else + { + directConnectStatus = "✗ 创建失败,端口可能被占用"; + } + } + + GUILayout.Space(5); + + // 加入房间 + if (GUILayout.Button("🔗 加入房间 (作为客户端)", buttonStyle)) + { + if (string.IsNullOrWhiteSpace(directConnectIP)) + { + directConnectStatus = "✗ 请输入主机 IP 地址"; + } + else + { + int port = int.TryParse(directConnectPort, out int p) ? p : DirectConnection.DEFAULT_PORT; + directConnectStatus = $"正在连接 {directConnectIP}:{port}..."; + + if (DirectConnection.Connect(directConnectIP, port)) + { + directConnectStatus = $"✓ 已连接到 {directConnectIP}:{port}"; + } + else + { + directConnectStatus = "✗ 连接失败,请检查 IP 和端口"; + } + } + } + } + else + { + // 已连接状态 + GUILayout.BeginVertical(boxStyle); + if (DirectConnection.IsServerRunning) + { + GUILayout.Label($"● 服务器运行中", labelStyle); + GUILayout.Label($"已连接客户端: {DirectConnection.GetConnectedClientCount()}", labelStyle); + } + else + { + GUILayout.Label($"● 已连接到主机", labelStyle); + } + GUILayout.EndVertical(); + + GUILayout.Space(10); + + // 断开按钮 + GUI.backgroundColor = new Color(1f, 0.5f, 0.5f); + if (GUILayout.Button("❌ 断开连接 / 关闭房间", buttonStyle)) + { + if (DirectConnection.IsServerRunning) + { + DirectConnection.StopServer(); + directConnectStatus = "房间已关闭"; + } + else + { + DirectConnection.Disconnect(); + directConnectStatus = "已断开连接"; + } + } + GUI.backgroundColor = Color.white; + } + + GUILayout.Space(10); + + // 状态消息 + if (!string.IsNullOrEmpty(directConnectStatus)) + { + GUILayout.Label(directConnectStatus, labelStyle); + } + + GUILayout.FlexibleSpace(); + + // 关闭按钮 + if (GUILayout.Button("关闭窗口", buttonStyle)) + { + showDirectConnect = false; + } + + GUI.DragWindow(); + } + + #endregion + + private void DrawPlayerList() + { + GUILayout.Label("Players in Lobby:", GUI.skin.label); + + var players = SteamLobby.GetAllLobbyMembers(); + if (players.Count == 0) + { + GUILayout.Label("", GUI.skin.label); + } + else + { + foreach (CSteamID playerId in players) + { + var playerName = SteamFriends.GetFriendPersonaName(playerId); + string prefix = (MultiplayerSession.HostSteamID == playerId) ? "[HOST] " : ""; + GUILayout.Label($"{prefix}{playerName} ({playerId})", GUI.skin.label); + } + } + + // 直连模式下显示客户端数量 + if (DirectConnection.Mode == ConnectionMode.DirectIP && DirectConnection.IsServerRunning) + { + GUILayout.Space(5); + GUILayout.Label($"[直连] 客户端数量: {DirectConnection.GetConnectedClientCount()}", + new GUIStyle(GUI.skin.label) { richText = true }); + } + } + } } diff --git a/ClassLibrary1/Networking/DirectConnection.cs b/ClassLibrary1/Networking/DirectConnection.cs new file mode 100644 index 00000000..3585f51b --- /dev/null +++ b/ClassLibrary1/Networking/DirectConnection.cs @@ -0,0 +1,527 @@ +using ONI_MP.DebugTools; +using ONI_MP.Networking.Packets.Architecture; +using ONI_MP.Networking.States; +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace ONI_MP.Networking +{ + public enum ConnectionMode + { + SteamP2P, + DirectIP + } + + public static class DirectConnection + { + public const int DEFAULT_PORT = 11000; + + private static TcpListener _server; + private static TcpClient _client; + private static NetworkStream _stream; + private static Thread _listenThread; + private static Thread _receiveThread; + private static bool _isRunning; + + // 存储已连接的客户端(主机用) + private static readonly ConcurrentDictionary _connectedClients + = new ConcurrentDictionary(); + + // 当前连接模式 + public static ConnectionMode Mode { get; set; } = ConnectionMode.SteamP2P; + + // 连接状态 + public static bool IsDirectConnected + { + get { return (_client != null && _client.Connected) || _connectedClients.Count > 0; } + } + + public static bool IsServerRunning + { + get { return _isRunning && _server != null; } + } + + public static bool IsClientConnected + { + get { return _isRunning && _client != null && _client.Connected; } + } + + #region 主机端 + + /// + /// 作为主机启动直连服务器 + /// + public static bool StartServer(int port) + { + if (_isRunning) + { + DebugConsole.LogWarning("[DirectConnection] Server already running"); + return false; + } + + try + { + _server = new TcpListener(IPAddress.Any, port); + _server.Start(); + _isRunning = true; + + _listenThread = new Thread(ListenForClients); + _listenThread.IsBackground = true; + _listenThread.Name = "DirectConnection_Listen"; + _listenThread.Start(); + + Mode = ConnectionMode.DirectIP; + MultiplayerSession.InSession = true; + + DebugConsole.Log("[DirectConnection] Server started on port " + port); + DebugConsole.Log("[DirectConnection] Your IP: " + GetLocalIPAddress() + ":" + port); + + return true; + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Failed to start server: " + ex.Message); + return false; + } + } + + public static bool StartServer() + { + return StartServer(DEFAULT_PORT); + } + + private static void ListenForClients() + { + while (_isRunning) + { + try + { + if (_server != null && _server.Pending()) + { + TcpClient client = _server.AcceptTcpClient(); + + string clientId; + IPEndPoint remoteEndPoint = client.Client.RemoteEndPoint as IPEndPoint; + if (remoteEndPoint != null) + { + clientId = remoteEndPoint.ToString(); + } + else + { + clientId = Guid.NewGuid().ToString(); + } + + _connectedClients[clientId] = client; + DebugConsole.Log("[DirectConnection] Client connected: " + clientId); + + // 为每个客户端启动接收线程 + ClientReceiveThreadData threadData = new ClientReceiveThreadData(); + threadData.ClientId = clientId; + threadData.Client = client; + + Thread clientThread = new Thread(ReceiveFromClientThread); + clientThread.IsBackground = true; + clientThread.Name = "DirectConnection_Client_" + clientId; + clientThread.Start(threadData); + } + Thread.Sleep(10); + } + catch (Exception ex) + { + if (_isRunning) + DebugConsole.LogError("[DirectConnection] Listen error: " + ex.Message); + } + } + } + + private class ClientReceiveThreadData + { + public string ClientId; + public TcpClient Client; + } + + private static void ReceiveFromClientThread(object data) + { + ClientReceiveThreadData threadData = (ClientReceiveThreadData)data; + ReceiveFromClient(threadData.ClientId, threadData.Client); + } + + private static void ReceiveFromClient(string clientId, TcpClient client) + { + NetworkStream stream = null; + try + { + stream = client.GetStream(); + byte[] lengthBuffer = new byte[4]; + + while (_isRunning && client.Connected) + { + try + { + if (stream.DataAvailable) + { + // 读取数据包长度 + int bytesRead = stream.Read(lengthBuffer, 0, 4); + if (bytesRead == 0) break; + + int packetLength = BitConverter.ToInt32(lengthBuffer, 0); + + if (packetLength <= 0 || packetLength > 10 * 1024 * 1024) + { + DebugConsole.LogWarning("[DirectConnection] Invalid packet length: " + packetLength); + break; + } + + // 读取数据包内容 + byte[] packetData = new byte[packetLength]; + int totalRead = 0; + while (totalRead < packetLength) + { + int read = stream.Read(packetData, totalRead, packetLength - totalRead); + if (read == 0) break; + totalRead = totalRead + read; + } + + if (totalRead == packetLength) + { + try + { + PacketHandler.HandleIncoming(packetData); + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Packet handling error: " + ex.Message); + } + } + } + Thread.Sleep(1); + } + catch (Exception ex) + { + if (_isRunning) + DebugConsole.LogError("[DirectConnection] Receive error from " + clientId + ": " + ex.Message); + break; + } + } + } + finally + { + TcpClient removed; + _connectedClients.TryRemove(clientId, out removed); + try { client.Close(); } + catch { } + DebugConsole.Log("[DirectConnection] Client disconnected: " + clientId); + } + } + + /// + /// 停止直连服务器 + /// + public static void StopServer() + { + _isRunning = false; + + foreach (TcpClient client in _connectedClients.Values) + { + try { client.Close(); } + catch { } + } + _connectedClients.Clear(); + + if (_server != null) + { + try { _server.Stop(); } + catch { } + _server = null; + } + + Mode = ConnectionMode.SteamP2P; + MultiplayerSession.InSession = false; + + DebugConsole.Log("[DirectConnection] Server stopped"); + } + + #endregion + + #region 客户端 + + /// + /// 作为客户端连接到主机 + /// + public static bool Connect(string ip, int port) + { + if (_isRunning) + { + DebugConsole.LogWarning("[DirectConnection] Already connected"); + return false; + } + + try + { + DebugConsole.Log("[DirectConnection] Connecting to " + ip + ":" + port + "..."); + + _client = new TcpClient(); + _client.Connect(ip, port); + _stream = _client.GetStream(); + _isRunning = true; + + _receiveThread = new Thread(ReceiveFromServer); + _receiveThread.IsBackground = true; + _receiveThread.Name = "DirectConnection_Receive"; + _receiveThread.Start(); + + Mode = ConnectionMode.DirectIP; + MultiplayerSession.InSession = true; + + GameClient.SetState(ClientState.Connected); + + DebugConsole.Log("[DirectConnection] Connected to " + ip + ":" + port); + + return true; + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Failed to connect: " + ex.Message); + Disconnect(); + return false; + } + } + + public static bool Connect(string ip) + { + return Connect(ip, DEFAULT_PORT); + } + + private static void ReceiveFromServer() + { + byte[] lengthBuffer = new byte[4]; + + while (_isRunning && _client != null && _client.Connected) + { + try + { + if (_stream != null && _stream.DataAvailable) + { + int bytesRead = _stream.Read(lengthBuffer, 0, 4); + if (bytesRead == 0) break; + + int packetLength = BitConverter.ToInt32(lengthBuffer, 0); + + if (packetLength <= 0 || packetLength > 10 * 1024 * 1024) + { + DebugConsole.LogWarning("[DirectConnection] Invalid packet length: " + packetLength); + break; + } + + byte[] packetData = new byte[packetLength]; + int totalRead = 0; + while (totalRead < packetLength) + { + int read = _stream.Read(packetData, totalRead, packetLength - totalRead); + if (read == 0) break; + totalRead = totalRead + read; + } + + if (totalRead == packetLength) + { + try + { + PacketHandler.HandleIncoming(packetData); + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Packet handling error: " + ex.Message); + } + } + } + Thread.Sleep(1); + } + catch (Exception ex) + { + if (_isRunning) + DebugConsole.LogError("[DirectConnection] Receive error: " + ex.Message); + break; + } + } + + Disconnect(); + } + + /// + /// 断开连接 + /// + public static void Disconnect() + { + bool wasConnected = _isRunning; + _isRunning = false; + + if (_stream != null) + { + try { _stream.Close(); } + catch { } + _stream = null; + } + + if (_client != null) + { + try { _client.Close(); } + catch { } + _client = null; + } + + Mode = ConnectionMode.SteamP2P; + MultiplayerSession.InSession = false; + GameClient.SetState(ClientState.Disconnected); + + if (wasConnected) + { + DebugConsole.Log("[DirectConnection] Disconnected"); + } + } + + #endregion + + #region 发送数据 + + /// + /// 发送数据包(客户端 -> 主机) + /// + public static bool SendToServer(byte[] data) + { + if (_stream == null || _client == null || !_client.Connected) + { + DebugConsole.LogWarning("[DirectConnection] Cannot send: not connected to server"); + return false; + } + + try + { + lock (_stream) + { + byte[] lengthPrefix = BitConverter.GetBytes(data.Length); + _stream.Write(lengthPrefix, 0, 4); + _stream.Write(data, 0, data.Length); + _stream.Flush(); + } + return true; + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Send to server error: " + ex.Message); + return false; + } + } + + /// + /// 发送数据包给指定客户端(主机用) + /// + public static bool SendToClient(string clientId, byte[] data) + { + TcpClient client; + if (!_connectedClients.TryGetValue(clientId, out client) || client == null) + { + DebugConsole.LogWarning("[DirectConnection] Client not found: " + clientId); + return false; + } + + try + { + NetworkStream stream = client.GetStream(); + lock (stream) + { + byte[] lengthPrefix = BitConverter.GetBytes(data.Length); + stream.Write(lengthPrefix, 0, 4); + stream.Write(data, 0, data.Length); + stream.Flush(); + } + return true; + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Send to " + clientId + " error: " + ex.Message); + return false; + } + } + + /// + /// 广播给所有客户端(主机用) + /// + public static void Broadcast(byte[] data) + { + foreach (var kvp in _connectedClients) + { + SendToClient(kvp.Key, data); + } + } + + #endregion + + #region 工具方法 + + /// + /// 获取本机局域网 IP + /// + public static string GetLocalIPAddress() + { + try + { + IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName()); + foreach (IPAddress ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork && + !IPAddress.IsLoopback(ip)) + { + string ipStr = ip.ToString(); + if (ipStr.StartsWith("192.168.") || + ipStr.StartsWith("10.") || + ipStr.StartsWith("172.")) + { + return ipStr; + } + } + } + foreach (IPAddress ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + return ip.ToString(); + } + } + } + catch (Exception ex) + { + DebugConsole.LogError("[DirectConnection] Failed to get local IP: " + ex.Message); + } + return "127.0.0.1"; + } + + /// + /// 获取已连接的客户端数量 + /// + public static int GetConnectedClientCount() + { + return _connectedClients.Count; + } + + /// + /// 获取连接状态信息 + /// + public static string GetConnectionInfo() + { + if (IsServerRunning) + { + return "Server running - " + GetConnectedClientCount() + " clients"; + } + else if (IsClientConnected) + { + return "Connected to host"; + } + return "Not connected"; + } + + #endregion + } +} diff --git a/ClassLibrary1/Networking/Packets/Architecture/PacketSender.cs b/ClassLibrary1/Networking/Packets/Architecture/PacketSender.cs index 5de74185..e0c1629b 100644 --- a/ClassLibrary1/Networking/Packets/Architecture/PacketSender.cs +++ b/ClassLibrary1/Networking/Packets/Architecture/PacketSender.cs @@ -1,4 +1,4 @@ -using ONI_MP.DebugTools; +using ONI_MP.DebugTools; using ONI_MP.Networking.Packets; using ONI_MP.Networking.Packets.Architecture; using ONI_MP.Networking.Packets.Core; @@ -9,246 +9,346 @@ namespace ONI_MP.Networking { - public static class PacketSender - { - public static int MAX_PACKET_SIZE_RELIABLE = 512; - public static int MAX_PACKET_SIZE_UNRELIABLE = 1024; - - public static byte[] SerializePacket(IPacket packet) - { - using (var ms = new System.IO.MemoryStream()) - using (var writer = new System.IO.BinaryWriter(ms)) - { - int packet_type = PacketRegistry.GetPacketId(packet); - writer.Write(packet_type); - packet.Serialize(writer); - return ms.ToArray(); - } - } - - /// - /// Send to one connection by HSteamNetConnection handle. - /// - public static bool SendToConnection(HSteamNetConnection conn, IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) - { - var bytes = SerializePacket(packet); - var _sendType = (int)sendType; - - IntPtr unmanagedPointer = Marshal.AllocHGlobal(bytes.Length); - try - { - Marshal.Copy(bytes, 0, unmanagedPointer, bytes.Length); - - var result = SteamNetworkingSockets.SendMessageToConnection( - conn, unmanagedPointer, (uint)bytes.Length, _sendType, out long msgNum); - - bool sent = result == EResult.k_EResultOK; - - if (!sent) - { - // DebugConsole.LogError($"[Sockets] Failed to send {packet.Type} to conn {conn} ({Utils.FormatBytes(bytes.Length)} | result: {result})", false); - } - else - { - PacketTracker.TrackSent(new PacketTracker.PacketTrackData { - packet = packet, - size = bytes.Length - }); - //DebugConsole.Log($"[Sockets] Sent {packet.Type} to conn {conn} ({Utils.FormatBytes(bytes.Length)})"); - } - return sent; - } - finally - { - Marshal.FreeHGlobal(unmanagedPointer); - } - } - - /// - /// Send a packet to a player by their SteamID. - /// - public static bool SendToPlayer(CSteamID steamID, IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) - { - if (!MultiplayerSession.ConnectedPlayers.TryGetValue(steamID, out var player) || player.Connection == null) - { - DebugConsole.LogWarning($"[PacketSender] No connection found for SteamID {steamID}"); - return false; - } - - return SendToConnection(player.Connection.Value, packet, sendType); - } - - public static void SendToHost(IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) - { - if (!MultiplayerSession.HostSteamID.IsValid()) - { - DebugConsole.LogWarning($"[PacketSender] Failed to send to host. Host is invalid."); - return; - } - SendToPlayer(MultiplayerSession.HostSteamID, packet, sendType); - } - - /// Original single-exclude overload - public static void SendToAll(IPacket packet, CSteamID? exclude = null, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) - { - foreach (var player in MultiplayerSession.ConnectedPlayers.Values) - { - if (exclude.HasValue && player.SteamID == exclude.Value) - continue; - - if (player.Connection != null) - SendToConnection(player.Connection.Value, packet, sendType); - } - } - - public static void SendToAllClients(IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) - { - if (!MultiplayerSession.IsHost) - { - DebugConsole.LogWarning("[PacketSender] Only the host can send to all clients"); - return; - } - SendToAll(packet, MultiplayerSession.HostSteamID, sendType); - } - - public static void SendToAllExcluding(IPacket packet, HashSet excludedIds, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) - { - foreach (var player in MultiplayerSession.ConnectedPlayers.Values) - { - if (excludedIds != null && excludedIds.Contains(player.SteamID)) - continue; - - if (player.Connection != null) - SendToConnection(player.Connection.Value, packet, sendType); - } - } - - /// - /// Sends a packet to all other players. - /// if sent from the host, it goes to all clients. - /// otherwise it is wrapped in a HostBroadcastPacket and sent to the host for rebroadcasting. - /// - /// - public static void SendToAllOtherPeers(IPacket packet) - { - if (!MultiplayerSession.InSession) - { - DebugConsole.LogWarning("[PacketSender] Not in a multiplayer session, cannot send to other peers"); - return; - } - DebugConsole.Log("[PacketSender] Sending packet to all other peers: " + packet.GetType().Name); - - if (MultiplayerSession.IsHost) - SendToAllClients(packet); - else - SendToHost(new HostBroadcastPacket(packet, MultiplayerSession.LocalSteamID)); - } - - public static void SendToAllOtherPeers_API(object api_packet) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToAllOtherPeers(packet); - } - - /// - /// custom types, interfaces and enums are not directly usable across assembly boundaries - /// - /// data object of the packet class that got registered with a ModApiPacket wrapper earlier - /// - /// - public static void SendToAll_API(object api_packet, CSteamID? exclude = null, int sendType = (int)SteamNetworkingSend.Reliable) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToAll(packet, exclude, (SteamNetworkingSend)sendType); - } - - public static void SendToAllClients_API(object api_packet, int sendType = (int)SteamNetworkingSend.Reliable) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToAllClients(packet, (SteamNetworkingSend)sendType); - } - - public static void SendToAllExcluding_API(object api_packet, HashSet excludedIds, int sendType = (int)SteamNetworkingSend.Reliable) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToAllExcluding(packet, excludedIds, (SteamNetworkingSend)sendType); - } - - public static void SendToPlayer_API(CSteamID steamID, object api_packet, int sendType = (int)SteamNetworkingSend.ReliableNoNagle) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToPlayer(steamID, packet, (SteamNetworkingSend)sendType); - } - - public static void SendToHost_API(object api_packet, int sendType = (int)SteamNetworkingSend.ReliableNoNagle) - { - var type = api_packet.GetType(); - if (!PacketRegistry.HasRegisteredPacket(type)) - { - DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); - return; - } - - if (!API_Helper.WrapApiPacket(api_packet, out var packet)) - { - DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); - return; - } - SendToHost(packet, (SteamNetworkingSend)sendType); - } - - } + public static class PacketSender + { + public static int MAX_PACKET_SIZE_RELIABLE = 512; + public static int MAX_PACKET_SIZE_UNRELIABLE = 1024; + + public static byte[] SerializePacket(IPacket packet) + { + using (var ms = new System.IO.MemoryStream()) + using (var writer = new System.IO.BinaryWriter(ms)) + { + int packet_type = PacketRegistry.GetPacketId(packet); + writer.Write(packet_type); + packet.Serialize(writer); + return ms.ToArray(); + } + } + + /// + /// Send to one connection by HSteamNetConnection handle. + /// + public static bool SendToConnection(HSteamNetConnection conn, IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) + { + // 如果是直连模式,使用直连发送 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + return SendViaDirectConnection(packet, null); + } + + var bytes = SerializePacket(packet); + var _sendType = (int)sendType; + + IntPtr unmanagedPointer = Marshal.AllocHGlobal(bytes.Length); + try + { + Marshal.Copy(bytes, 0, unmanagedPointer, bytes.Length); + + var result = SteamNetworkingSockets.SendMessageToConnection( + conn, unmanagedPointer, (uint)bytes.Length, _sendType, out long msgNum); + + bool sent = result == EResult.k_EResultOK; + + if (!sent) + { + // DebugConsole.LogError($"[Sockets] Failed to send {packet.Type} to conn {conn} ({Utils.FormatBytes(bytes.Length)} | result: {result})", false); + } + else + { + PacketTracker.TrackSent(new PacketTracker.PacketTrackData { + packet = packet, + size = bytes.Length + }); + } + return sent; + } + finally + { + Marshal.FreeHGlobal(unmanagedPointer); + } + } + + /// + /// Send a packet to a player by their SteamID. + /// + public static bool SendToPlayer(CSteamID steamID, IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) + { + // 如果是直连模式 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + return SendViaDirectConnection(packet, steamID.ToString()); + } + + if (!MultiplayerSession.ConnectedPlayers.TryGetValue(steamID, out var player) || player.Connection == null) + { + DebugConsole.LogWarning($"[PacketSender] No connection found for SteamID {steamID}"); + return false; + } + + return SendToConnection(player.Connection.Value, packet, sendType); + } + + public static void SendToHost(IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.ReliableNoNagle) + { + // 如果是直连模式,客户端直接发给服务器 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + SendViaDirectConnection(packet, null, isToHost: true); + return; + } + + if (!MultiplayerSession.HostSteamID.IsValid()) + { + DebugConsole.LogWarning($"[PacketSender] Failed to send to host. Host is invalid."); + return; + } + SendToPlayer(MultiplayerSession.HostSteamID, packet, sendType); + } + + /// Original single-exclude overload + public static void SendToAll(IPacket packet, CSteamID? exclude = null, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) + { + // 如果是直连模式,广播给所有客户端 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + BroadcastViaDirectConnection(packet); + return; + } + + foreach (var player in MultiplayerSession.ConnectedPlayers.Values) + { + if (exclude.HasValue && player.SteamID == exclude.Value) + continue; + + if (player.Connection != null) + SendToConnection(player.Connection.Value, packet, sendType); + } + } + + public static void SendToAllClients(IPacket packet, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) + { + if (!MultiplayerSession.IsHost) + { + DebugConsole.LogWarning("[PacketSender] Only the host can send to all clients"); + return; + } + + // 如果是直连模式 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + BroadcastViaDirectConnection(packet); + return; + } + + SendToAll(packet, MultiplayerSession.HostSteamID, sendType); + } + + public static void SendToAllExcluding(IPacket packet, HashSet excludedIds, SteamNetworkingSend sendType = SteamNetworkingSend.Reliable) + { + // 直连模式暂时不支持排除,直接广播 + if (DirectConnection.Mode == ConnectionMode.DirectIP) + { + BroadcastViaDirectConnection(packet); + return; + } + + foreach (var player in MultiplayerSession.ConnectedPlayers.Values) + { + if (excludedIds != null && excludedIds.Contains(player.SteamID)) + continue; + + if (player.Connection != null) + SendToConnection(player.Connection.Value, packet, sendType); + } + } + + /// + /// Sends a packet to all other players. + /// if sent from the host, it goes to all clients. + /// otherwise it is wrapped in a HostBroadcastPacket and sent to the host for rebroadcasting. + /// + public static void SendToAllOtherPeers(IPacket packet) + { + if (!MultiplayerSession.InSession) + { + DebugConsole.LogWarning("[PacketSender] Not in a multiplayer session, cannot send to other peers"); + return; + } + DebugConsole.Log("[PacketSender] Sending packet to all other peers: " + packet.GetType().Name); + + if (MultiplayerSession.IsHost) + SendToAllClients(packet); + else + SendToHost(new HostBroadcastPacket(packet, MultiplayerSession.LocalSteamID)); + } + + #region Direct Connection Methods + + /// + /// 通过直连发送数据包 + /// + /// 要发送的数据包 + /// 目标客户端ID(主机用),null表示发给服务器(客户端用) + /// 是否发给主机 + private static bool SendViaDirectConnection(IPacket packet, string targetClientId, bool isToHost = false) + { + try + { + var bytes = SerializePacket(packet); + + if (isToHost || !MultiplayerSession.IsHost) + { + // 客户端发给主机 + DirectConnection.SendToServer(bytes); + } + else if (targetClientId != null) + { + // 主机发给特定客户端 + DirectConnection.SendToClient(targetClientId, bytes); + } + + PacketTracker.TrackSent(new PacketTracker.PacketTrackData + { + packet = packet, + size = bytes.Length + }); + + return true; + } + catch (Exception ex) + { + DebugConsole.LogError($"[PacketSender] Direct connection send failed: {ex.Message}"); + return false; + } + } + + /// + /// 通过直连广播给所有客户端(主机用) + /// + private static void BroadcastViaDirectConnection(IPacket packet) + { + try + { + var bytes = SerializePacket(packet); + DirectConnection.Broadcast(bytes); + + PacketTracker.TrackSent(new PacketTracker.PacketTrackData + { + packet = packet, + size = bytes.Length + }); + } + catch (Exception ex) + { + DebugConsole.LogError($"[PacketSender] Direct connection broadcast failed: {ex.Message}"); + } + } + + #endregion + + #region API Methods (保持不变) + + public static void SendToAllOtherPeers_API(object api_packet) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToAllOtherPeers(packet); + } + + public static void SendToAll_API(object api_packet, CSteamID? exclude = null, int sendType = (int)SteamNetworkingSend.Reliable) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToAll(packet, exclude, (SteamNetworkingSend)sendType); + } + + public static void SendToAllClients_API(object api_packet, int sendType = (int)SteamNetworkingSend.Reliable) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToAllClients(packet, (SteamNetworkingSend)sendType); + } + + public static void SendToAllExcluding_API(object api_packet, HashSet excludedIds, int sendType = (int)SteamNetworkingSend.Reliable) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToAllExcluding(packet, excludedIds, (SteamNetworkingSend)sendType); + } + + public static void SendToPlayer_API(CSteamID steamID, object api_packet, int sendType = (int)SteamNetworkingSend.ReliableNoNagle) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToPlayer(steamID, packet, (SteamNetworkingSend)sendType); + } + + public static void SendToHost_API(object api_packet, int sendType = (int)SteamNetworkingSend.ReliableNoNagle) + { + var type = api_packet.GetType(); + if (!PacketRegistry.HasRegisteredPacket(type)) + { + DebugConsole.LogError($"[PacketSender] Attempted to send unregistered packet type: {type.Name}"); + return; + } + + if (!API_Helper.WrapApiPacket(api_packet, out var packet)) + { + DebugConsole.LogError($"[PacketSender] Failed to wrap API packet of type: {type.Name}"); + return; + } + SendToHost(packet, (SteamNetworkingSend)sendType); + } + + #endregion + } } diff --git a/README.md b/README.md index 6ecc66f1..83a822be 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,103 @@ ![Logo](https://i.imgur.com/GCIbhpn.png) +# ONI Together - Direct Connection Fork + +This fork adds Direct IP Connection support to the ONI Together multiplayer mod. + +## Changes from Original + +### New Features +- Direct TCP connection (bypass Steam P2P) +- LAN play support without Steam relay +- Lower latency for local network play + +### Modified Files +- ClassLibrary1/Networking/DirectConnection.cs (NEW) +- ClassLibrary1/Networking/PacketSender.cs (MODIFIED) +- ClassLibrary1/DebugTools/DebugMenu.cs (MODIFIED) + +--- + +## Build Instructions + +### Requirements +- Visual Studio 2022 (or later) +- .NET Framework 4.7.2 +- Oxygen Not Included (Steam version) + +### Setup Steps + +1. Copy Directory.Build.props.default to Directory.Build.props.user + +2. Edit Directory.Build.props.user with your paths: + - GameLibsFolder: Your ONI installation Managed folder + - ModFolder: Your mods dev folder + +3. Open Oni_MP.sln in Visual Studio + +4. Build with Ctrl + Shift + B + +5. If first build fails, close VS and reopen, then build again + +--- + +## How to Use + +### In-Game Hotkeys +- Shift + F1: Open Debug Menu +- Shift + F2: Open Direct Connect Window + +### Host a Game +1. Press Shift + F2 +2. Click Create Room (Host) +3. Note your IP address (e.g., 192.168.1.100:11000) +4. Share IP with friend + +### Join a Game +1. Press Shift + F2 +2. Enter host IP address +3. Enter port (default: 11000) +4. Click Join Room (Join) + +--- + +## Network Setup + +### LAN Play +- Both players on same network +- Use local IP (192.168.x.x or 10.x.x.x) +- No additional setup needed + +### Internet Play +- Option A: Port forward 11000 TCP on router +- Option B: Use ZeroTier (https://www.zerotier.com/) +- Option C: Use Tailscale (https://tailscale.com/) + +--- + +## Troubleshooting + +### Build Errors +- Check paths in Directory.Build.props.user +- Run game once first to create Klei folders +- Close VS and reopen if first build fails + +### Connection Issues +- Check firewall allows port 11000 +- Verify IP address is correct +- Ensure both have same mod version + +--- + +## License +MIT License - Same as original project + +## Credits +- Original mod: Lyraedan +- Direct Connection: Added for LAN/IP play + + + # ONI Together: An Oxygen Not Included Multiplayer Mod (WIP) > **Note:** This is a work-in-progress project. Not to be confused with [onimp/oni_multiplayer](https://github.com/onimp/oni_multiplayer).