diff --git a/KCClient.cs b/KCClient.cs index fa52f44..75ba8ec 100644 --- a/KCClient.cs +++ b/KCClient.cs @@ -76,7 +76,19 @@ namespace KCM private static void Client_Connected(object sender, EventArgs e) { - + try + { + if (client != null && client.Connection != null) + { + client.Connection.CanQualityDisconnect = false; + client.Connection.MaxSendAttempts = 50; + } + } + catch (Exception ex) + { + Main.helper.Log("Error configuring client connection"); + Main.helper.Log(ex.ToString()); + } } diff --git a/KCServer.cs b/KCServer.cs index 6f90471..3a0de11 100644 --- a/KCServer.cs +++ b/KCServer.cs @@ -21,6 +21,9 @@ namespace KCM public static Server server = new Server(Main.steamServer); public static bool started = false; + private static readonly Dictionary> saveTransferQueues = new Dictionary>(); + private const int SaveTransferPacketsPerUpdatePerClient = 10; + static KCServer() { //server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined")); @@ -50,6 +53,7 @@ namespace KCM } ev.Client.CanQualityDisconnect = false; + ev.Client.MaxSendAttempts = 50; Main.helper.Log("Client ID is: " + ev.Client.Id); @@ -85,6 +89,8 @@ namespace KCM if (entry != null) Destroy(entry.gameObject); + saveTransferQueues.Remove(ev.Client.Id); + Main.helper.Log($"Client disconnected. {ev.Reason}"); } catch (Exception ex) @@ -125,6 +131,68 @@ namespace KCM private void Update() { server.Update(); + ProcessSaveTransfers(); + } + + private static void ProcessSaveTransfers() + { + if (!KCServer.IsRunning) + return; + + if (saveTransferQueues.Count == 0) + return; + + var clients = saveTransferQueues.Keys.ToList(); + foreach (var clientId in clients) + { + Queue queue; + if (!saveTransferQueues.TryGetValue(clientId, out queue) || queue == null) + continue; + + int sentThisUpdate = 0; + while (sentThisUpdate < SaveTransferPacketsPerUpdatePerClient && queue.Count > 0) + { + var packet = queue.Dequeue(); + packet.Send(clientId); + sentThisUpdate++; + } + + if (queue.Count == 0) + saveTransferQueues.Remove(clientId); + } + } + + public static void EnqueueSaveTransfer(ushort toClient, byte[] bytes) + { + if (bytes == null) + return; + + int chunkSize = 900; + int sent = 0; + int totalChunks = (int)Math.Ceiling((double)bytes.Length / chunkSize); + + var queue = new Queue(totalChunks); + for (int i = 0; i < totalChunks; i++) + { + int currentChunkSize = Math.Min(chunkSize, bytes.Length - sent); + var chunk = new byte[currentChunkSize]; + Array.Copy(bytes, sent, chunk, 0, currentChunkSize); + + queue.Enqueue(new SaveTransferPacket() + { + saveSize = bytes.Length, + saveDataChunk = chunk, + chunkId = i, + chunkSize = chunk.Length, + saveDataIndex = sent, + totalChunks = totalChunks + }); + + sent += currentChunkSize; + } + + saveTransferQueues[toClient] = queue; + Main.helper.Log($"Queued {totalChunks} save data chunks for client {toClient}"); } private void OnApplicationQuit() diff --git a/Main.cs b/Main.cs index 8d516c5..9cec6a3 100644 --- a/Main.cs +++ b/Main.cs @@ -55,6 +55,8 @@ namespace KCM public static Dictionary kCPlayers = new Dictionary(); public static Dictionary clientSteamIds = new Dictionary(); + private static readonly Dictionary lastTeamIdLookupLogMs = new Dictionary(); + public static KCPlayer GetPlayerByClientID(ushort clientId) { return kCPlayers[clientSteamIds[clientId]]; @@ -62,23 +64,33 @@ namespace KCM public static Player GetPlayerByTeamID(int teamId) // Need to replace building / production types so that the correct player is used. IResourceStorage and IResourceProvider, and jobs { - try - { - var player = kCPlayers.Values.FirstOrDefault(p => p.inst.PlayerLandmassOwner.teamId == teamId).inst; + KCPlayer match = kCPlayers.Values.FirstOrDefault(p => + p != null && + p.inst != null && + p.inst.PlayerLandmassOwner != null && + p.inst.PlayerLandmassOwner.teamId == teamId); - return player; - } - catch (Exception e) + if (match != null && match.inst != null) + return match.inst; + + if (KCServer.IsRunning || KCClient.client.IsConnected) { - if (KCServer.IsRunning || KCClient.client.IsConnected) + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + long last; + if (!lastTeamIdLookupLogMs.TryGetValue(teamId, out last) || (now - last) > 2000) { - Main.helper.Log("Failed finding player by teamID: " + teamId + " My teamID is: " + Player.inst.PlayerLandmassOwner.teamId); + lastTeamIdLookupLogMs[teamId] = now; + + string myTeamId = (Player.inst != null && Player.inst.PlayerLandmassOwner != null) + ? Player.inst.PlayerLandmassOwner.teamId.ToString() + : "unknown"; + + Main.helper.Log("Failed finding player by teamID: " + teamId + " My teamID is: " + myTeamId); Main.helper.Log(kCPlayers.Count.ToString()); - Main.helper.Log(string.Join(", ", kCPlayers.Values.Select(p => p.inst.PlayerLandmassOwner.teamId.ToString()))); - Main.helper.Log(e.Message); - Main.helper.Log(e.StackTrace); + Main.helper.Log(string.Join(", ", kCPlayers.Values.Where(p => p != null && p.inst != null && p.inst.PlayerLandmassOwner != null).Select(p => p.inst.PlayerLandmassOwner.teamId.ToString()))); } } + return Player.inst; } diff --git a/Packets/Handlers/PacketHandler.cs b/Packets/Handlers/PacketHandler.cs index bbafed6..d3864f7 100644 --- a/Packets/Handlers/PacketHandler.cs +++ b/Packets/Handlers/PacketHandler.cs @@ -222,7 +222,13 @@ namespace KCM.Packets.Handlers try { var packetRef = Packets[packet.packetId]; - Message message = Message.Create(MessageSendMode.Reliable, packet.packetId); + + MessageSendMode sendMode = MessageSendMode.Reliable; + Packet basePacket = packet as Packet; + if (basePacket != null) + sendMode = basePacket.sendMode; + + Message message = Message.Create(sendMode, packet.packetId); foreach (var prop in packetRef.properties) { diff --git a/Packets/Network/ClientConnected.cs b/Packets/Network/ClientConnected.cs index 879f600..9c8abac 100644 --- a/Packets/Network/ClientConnected.cs +++ b/Packets/Network/ClientConnected.cs @@ -92,36 +92,7 @@ namespace KCM.Packets.Network return; byte[] bytes = LoadSaveLoadAtPathHook.saveData; - int chunkSize = 900; // 900 bytes per chunk to fit within packet size limit - - List chunks = SplitByteArrayIntoChunks(bytes, chunkSize); - Main.helper.Log("Save Transfer started!"); - - int sent = 0; - int packetsSent = 0; - - for (int i = 0; i < chunks.Count; i++) - { - var chunk = chunks[i]; - - - new SaveTransferPacket() - { - saveSize = bytes.Length, - saveDataChunk = chunk, - chunkId = i, - chunkSize = chunk.Length, - saveDataIndex = sent, - totalChunks = chunks.Count - }.Send(clientId); - - Main.helper.Log(" "); - - packetsSent++; - sent += chunk.Length; - } - - Main.helper.Log($"Sent {packetsSent} save data chunks to client"); + KCServer.EnqueueSaveTransfer(clientId, bytes); } else { diff --git a/Packets/Packet.cs b/Packets/Packet.cs index 4cefd0e..69c61cb 100644 --- a/Packets/Packet.cs +++ b/Packets/Packet.cs @@ -11,6 +11,7 @@ namespace KCM.Packets { public abstract ushort packetId { get; } public ushort clientId { get; set; } + public virtual Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Reliable; public KCPlayer player { diff --git a/Packets/State/BuildingStatePacket.cs b/Packets/State/BuildingStatePacket.cs index 70ea74b..e120ba5 100644 --- a/Packets/State/BuildingStatePacket.cs +++ b/Packets/State/BuildingStatePacket.cs @@ -11,6 +11,7 @@ namespace KCM.Packets.State public class BuildingStatePacket : Packet { public override ushort packetId => (ushort)Enums.Packets.BuildingStatePacket; + public override Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Unreliable; public string customName { get; set; } public Guid guid { get; set; } diff --git a/README.md b/README.md index a2b0c32..4f8c8e5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ A mellékelt log (`output.txt`) alapján több tipikus hiba okozta a szerver ind - Mentés betöltéskor a `ProcessBuilding` útvonal kiegészítése `World.inst.PlaceFromLoad(...)` + `UnpackStage2(...)` hívásokkal (különösen fontos a “világba helyezés” mellékhatásai miatt, pl. farm/field regisztráció) - Save transfer kliens oldalon robusztusabb inicializálás/reset (ne ragadjon be a statikus állapot több betöltés után, plusz bounds/null ellenőrzések) - Kompatibilitási fix: `World.inst.liverySets` lista esetén `.Count` használata `.Length` helyett (különben `Compilation failed` lehet egyes verziókon) +- Hálózati stabilitás: `BuildingStatePacket` most `Unreliable` módban megy (state jellegű csomagoknál jobb, ha a legfrissebb állapot érkezik meg és nem torlódik fel a megbízható sor) +- Mentés-szinkron stabilitás: szerver oldalon a save chunkok már nem egy nagy for-ciklusban mennek ki, hanem ütemezve (csökkenti a “The gap between received sequence IDs…” / “Poor connection” diszkonnekteket) +- Kapcsolat tuning: kliens és szerver oldalon emelt `MaxSendAttempts`, és tiltott minőség-alapú auto-disconnect (különösen save transfer közben volt agresszív) Érintett fájlok (főbb pontok): @@ -36,6 +39,8 @@ A mellékelt log (`output.txt`) alapján több tipikus hiba okozta a szerver ind - `KCServer.cs` - `Packets/Handlers/LobbyHandler.cs` - `RiptideSteamTransport/LobbyManager.cs` +- `Packets/Handlers/PacketHandler.cs` +- `Packets/State/BuildingStatePacket.cs` ## Telepítés / használat