From 0776da883f5986457bb66d9b0c99d7edb7338e77 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 19:12:53 +0100 Subject: [PATCH] BIG FIX XD --- Attributes/NoServerRelayAttribute.cs | 10 + Enums/Packets.cs | 5 +- KCServer.cs | 1 + Packets/Handlers/PacketHandler.cs | 4 +- Packets/Lobby/SaveTransferPacket.cs | 8 + Packets/Lobby/WorldSeed.cs | 8 + Packets/Network/ResyncRequestPacket.cs | 35 ++ Packets/State/BuildingSnapshotPacket.cs | 33 ++ Packets/State/ResourceSnapshotPacket.cs | 37 ++ README.md | 5 + ServerLobby/ServerLobbyScript.cs | 15 + StateManagement/Sync/SyncManager.cs | 558 ++++++++++++++++++++++++ 12 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 Attributes/NoServerRelayAttribute.cs create mode 100644 Packets/Network/ResyncRequestPacket.cs create mode 100644 Packets/State/BuildingSnapshotPacket.cs create mode 100644 Packets/State/ResourceSnapshotPacket.cs create mode 100644 StateManagement/Sync/SyncManager.cs diff --git a/Attributes/NoServerRelayAttribute.cs b/Attributes/NoServerRelayAttribute.cs new file mode 100644 index 0000000..df2e6a8 --- /dev/null +++ b/Attributes/NoServerRelayAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace KCM.Attributes +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NoServerRelayAttribute : Attribute + { + } +} + diff --git a/Enums/Packets.cs b/Enums/Packets.cs index f7da401..04eadfd 100644 --- a/Enums/Packets.cs +++ b/Enums/Packets.cs @@ -44,6 +44,9 @@ namespace KCM.Enums AddVillager = 88, SetupInitialWorkers = 89, VillagerTeleportTo = 90, - PlaceKeepRandomly = 91 + PlaceKeepRandomly = 91, + ResyncRequest = 92, + ResourceSnapshot = 93, + BuildingSnapshot = 94 } } diff --git a/KCServer.cs b/KCServer.cs index 3a0de11..685f347 100644 --- a/KCServer.cs +++ b/KCServer.cs @@ -132,6 +132,7 @@ namespace KCM { server.Update(); ProcessSaveTransfers(); + KCM.StateManagement.Sync.SyncManager.ServerUpdate(); } private static void ProcessSaveTransfers() diff --git a/Packets/Handlers/PacketHandler.cs b/Packets/Handlers/PacketHandler.cs index eb50df4..90a91ef 100644 --- a/Packets/Handlers/PacketHandler.cs +++ b/Packets/Handlers/PacketHandler.cs @@ -139,7 +139,9 @@ namespace KCM.Packets.Handlers { packet.HandlePacketServer(); - ((Packet)packet).SendToAll(); + bool shouldRelay = packet.GetType().GetCustomAttributes(typeof(NoServerRelayAttribute), inherit: true).Length == 0; + if (shouldRelay) + ((Packet)packet).SendToAll(); } catch (Exception ex) { diff --git a/Packets/Lobby/SaveTransferPacket.cs b/Packets/Lobby/SaveTransferPacket.cs index 114fc8d..c4707b7 100644 --- a/Packets/Lobby/SaveTransferPacket.cs +++ b/Packets/Lobby/SaveTransferPacket.cs @@ -112,6 +112,14 @@ namespace KCM.Packets.Lobby LoadSaveLoadHook.saveContainer.Unpack(null); Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); + try + { + new KCM.Packets.Network.ResyncRequestPacket { reason = "post-load" }.Send(); + } + catch + { + } + if (ServerLobbyScript.LoadingSave != null) ServerLobbyScript.LoadingSave.SetActive(false); diff --git a/Packets/Lobby/WorldSeed.cs b/Packets/Lobby/WorldSeed.cs index 38655c9..f3970f4 100644 --- a/Packets/Lobby/WorldSeed.cs +++ b/Packets/Lobby/WorldSeed.cs @@ -33,6 +33,14 @@ namespace KCM.Packets.Lobby World.inst.Generate(Seed); Vector3 center = World.inst.GetCellData(World.inst.GridWidth / 2, World.inst.GridHeight / 2).Center; Cam.inst.SetTrackingPos(center); + + try + { + new KCM.Packets.Network.ResyncRequestPacket { reason = "post-world-seed" }.Send(); + } + catch + { + } } catch (Exception e) { diff --git a/Packets/Network/ResyncRequestPacket.cs b/Packets/Network/ResyncRequestPacket.cs new file mode 100644 index 0000000..f810989 --- /dev/null +++ b/Packets/Network/ResyncRequestPacket.cs @@ -0,0 +1,35 @@ +using KCM.Attributes; +using KCM.StateManagement.Sync; +using System; + +namespace KCM.Packets.Network +{ + [NoServerRelay] + public class ResyncRequestPacket : Packet + { + public override ushort packetId => (ushort)Enums.Packets.ResyncRequest; + + public string reason { get; set; } + + public override void HandlePacketClient() + { + } + + public override void HandlePacketServer() + { + try + { + if (!KCServer.IsRunning) + return; + + SyncManager.SendResyncToClient(clientId, reason); + } + catch (Exception ex) + { + Main.helper.Log("Error handling ResyncRequestPacket on server"); + Main.helper.Log(ex.ToString()); + } + } + } +} + diff --git a/Packets/State/BuildingSnapshotPacket.cs b/Packets/State/BuildingSnapshotPacket.cs new file mode 100644 index 0000000..2f5375e --- /dev/null +++ b/Packets/State/BuildingSnapshotPacket.cs @@ -0,0 +1,33 @@ +using KCM.StateManagement.Sync; +using System; + +namespace KCM.Packets.State +{ + public class BuildingSnapshotPacket : Packet + { + public override ushort packetId => (ushort)Enums.Packets.BuildingSnapshot; + + public byte[] payload { get; set; } + + public override void HandlePacketClient() + { + try + { + if (payload == null || payload.Length == 0) + return; + + SyncManager.ApplyBuildingSnapshot(payload); + } + catch (Exception ex) + { + Main.helper.Log("Error applying BuildingSnapshotPacket"); + Main.helper.Log(ex.ToString()); + } + } + + public override void HandlePacketServer() + { + } + } +} + diff --git a/Packets/State/ResourceSnapshotPacket.cs b/Packets/State/ResourceSnapshotPacket.cs new file mode 100644 index 0000000..e84800e --- /dev/null +++ b/Packets/State/ResourceSnapshotPacket.cs @@ -0,0 +1,37 @@ +using KCM.StateManagement.Sync; +using System; +using System.Collections.Generic; + +namespace KCM.Packets.State +{ + public class ResourceSnapshotPacket : Packet + { + public override ushort packetId => (ushort)Enums.Packets.ResourceSnapshot; + + public override Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Unreliable; + + public List resourceTypes { get; set; } + public List amounts { get; set; } + + public override void HandlePacketClient() + { + try + { + if (KCClient.client.IsConnected && KCClient.client.Id == clientId) + return; + + SyncManager.ApplyResourceSnapshot(resourceTypes, amounts); + } + catch (Exception ex) + { + Main.helper.Log("Error applying ResourceSnapshotPacket"); + Main.helper.Log(ex.ToString()); + } + } + + public override void HandlePacketServer() + { + } + } +} + diff --git a/README.md b/README.md index 38481a0..621db09 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Ha a `output.txt` logban `Compilation failed` szerepel, akkor a mod **nem tölt - Épület lerakás események (alap meta: `uniqueName`, `guid`, pozíció/rotáció). - Épület állapot frissítések “snapshot” jelleggel (`BuildingStatePacket`): built/placed, constructionProgress, life, stb. - Néhány globális esemény: idősebesség változtatás, időjárás váltás, fa kivágás (repo verziótól függően). +- Host oldalon periodikus *resource snapshot* korrigálás (ha drift/desync van, visszahúzza a klienst). **Mentés betöltés (host → kliens)** - Host oldalon a mentés byte-ok chunkolva kerülnek kiküldésre (`SaveTransferPacket`). @@ -68,3 +69,7 @@ Bug reporthoz küldd el: ## Fejlesztés Repo-szabályok és szerkezet: `AGENTS.md`. + +### Gyors resync + +A lobby chatben írd be: `/resync` – a kliens kér egy resync-et a hosttól (resource + building + villager “teleport” snapshot). diff --git a/ServerLobby/ServerLobbyScript.cs b/ServerLobby/ServerLobbyScript.cs index a4990fa..c1bfbb5 100644 --- a/ServerLobby/ServerLobbyScript.cs +++ b/ServerLobby/ServerLobbyScript.cs @@ -219,6 +219,21 @@ namespace KCM { if (ChatInput.text.Length > 0) { + if (ChatInput.text.Trim().Equals("/resync", StringComparison.OrdinalIgnoreCase)) + { + try + { + new KCM.Packets.Network.ResyncRequestPacket { reason = "manual:/resync" }.Send(); + LobbyHandler.AddSystemMessage("Resync requested."); + } + catch + { + } + + ChatInput.text = ""; + return; + } + new ChatMessage() { PlayerName = KCClient.inst.Name, diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs new file mode 100644 index 0000000..fd0850b --- /dev/null +++ b/StateManagement/Sync/SyncManager.cs @@ -0,0 +1,558 @@ +using Assets.Code; +using KCM.Packets.Game.GameVillager; +using KCM.Packets.State; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace KCM.StateManagement.Sync +{ + public static class SyncManager + { + private const int ResourceBroadcastIntervalMs = 2000; + private const int MaxBuildingSnapshotBytes = 30000; + private const int MaxVillagerTeleportsPerResync = 400; + + private static long lastResourceBroadcastMs; + + private static FieldInfo freeResourceAmountField; + private static MethodInfo resourceAmountGetMethod; + private static MethodInfo resourceAmountSetMethod; + private static MethodInfo freeResourceManagerMaybeRefresh; + private static MethodInfo fieldSystemMaybeRefresh; + + public static void ServerUpdate() + { + if (!KCServer.IsRunning) + return; + + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastResourceBroadcastMs) < ResourceBroadcastIntervalMs) + return; + + lastResourceBroadcastMs = now; + + try + { + ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket(); + if (snapshot == null) + return; + + snapshot.clientId = KCClient.client != null ? KCClient.client.Id : (ushort)0; + + // Exclude host/local client from receiving its own snapshot. + ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0; + snapshot.SendToAll(exceptId); + } + catch (Exception ex) + { + Main.helper.Log("Error broadcasting resource snapshot"); + Main.helper.Log(ex.ToString()); + } + } + + public static void SendResyncToClient(ushort toClient, string reason) + { + if (!KCServer.IsRunning) + return; + + Main.helper.Log($"Resync requested by client {toClient} ({reason ?? ""})"); + + try + { + ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket(); + if (snapshot != null) + { + snapshot.clientId = KCClient.client != null ? KCClient.client.Id : (ushort)0; + snapshot.Send(toClient); + } + } + catch (Exception ex) + { + Main.helper.Log("Error sending resource resync"); + Main.helper.Log(ex.ToString()); + } + + try + { + SendBuildingSnapshotToClient(toClient); + } + catch (Exception ex) + { + Main.helper.Log("Error sending building resync"); + Main.helper.Log(ex.ToString()); + } + + try + { + SendVillagerTeleportSnapshotToClient(toClient); + } + catch (Exception ex) + { + Main.helper.Log("Error sending villager resync"); + Main.helper.Log(ex.ToString()); + } + } + + private static ResourceSnapshotPacket BuildResourceSnapshotPacket() + { + List types; + List amounts; + if (!TryReadFreeResources(out types, out amounts)) + return null; + + return new ResourceSnapshotPacket + { + resourceTypes = types, + amounts = amounts + }; + } + + private static void SendBuildingSnapshotToClient(ushort toClient) + { + List buildings = new List(); + + foreach (var p in Main.kCPlayers.Values) + { + if (p == null || p.inst == null) + continue; + + try + { + var list = p.inst.Buildings; + for (int i = 0; i < list.Count; i++) + { + Building b = list.data[i]; + if (b != null) + buildings.Add(b); + } + } + catch + { + } + } + + if (buildings.Count == 0) + return; + + int idx = 0; + while (idx < buildings.Count) + { + byte[] payload = BuildBuildingSnapshotPayload(buildings, ref idx); + if (payload == null || payload.Length == 0) + break; + + new BuildingSnapshotPacket { payload = payload }.Send(toClient); + } + } + + private static byte[] BuildBuildingSnapshotPayload(List buildings, ref int startIndex) + { + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + long countPos = ms.Position; + bw.Write(0); // placeholder for record count + int written = 0; + + for (; startIndex < buildings.Count; startIndex++) + { + Building b = buildings[startIndex]; + if (b == null) + continue; + + long before = ms.Position; + try + { + WriteBuildingRecord(bw, b); + written++; + } + catch + { + ms.Position = before; + ms.SetLength(before); + } + + if (ms.Length >= MaxBuildingSnapshotBytes) + { + startIndex++; + break; + } + } + + long endPos = ms.Position; + ms.Position = countPos; + bw.Write(written); + ms.Position = endPos; + + return ms.ToArray(); + } + } + + private static void WriteBuildingRecord(BinaryWriter bw, Building b) + { + bw.Write(b.TeamID()); + bw.Write(b.guid.ToByteArray()); + + bw.Write(b.UniqueName ?? ""); + bw.Write(b.customName ?? ""); + + Vector3 globalPosition = b.transform.position; + Quaternion rotation = b.transform.childCount > 0 ? b.transform.GetChild(0).rotation : b.transform.rotation; + Vector3 localPosition = b.transform.childCount > 0 ? b.transform.GetChild(0).localPosition : Vector3.zero; + + bw.Write(globalPosition.x); + bw.Write(globalPosition.y); + bw.Write(globalPosition.z); + + bw.Write(rotation.x); + bw.Write(rotation.y); + bw.Write(rotation.z); + bw.Write(rotation.w); + + bw.Write(localPosition.x); + bw.Write(localPosition.y); + bw.Write(localPosition.z); + + bw.Write(b.IsBuilt()); + bw.Write(b.IsPlaced()); + bw.Write(b.Open); + bw.Write(b.doBuildAnimation); + bw.Write(b.constructionPaused); + bw.Write(b.constructionProgress); + + float resourceProgress = 0f; + try + { + var field = b.GetType().GetField("resourceProgress", BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null) + resourceProgress = (float)field.GetValue(b); + } + catch { } + bw.Write(resourceProgress); + + bw.Write(b.Life); + bw.Write(b.ModifiedMaxLife); + + int yearBuilt = 0; + try + { + var field = b.GetType().GetField("yearBuilt", BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null) + yearBuilt = (int)field.GetValue(b); + } + catch { } + bw.Write(yearBuilt); + + bw.Write(b.decayProtection); + } + + public static void ApplyBuildingSnapshot(byte[] payload) + { + using (var ms = new MemoryStream(payload)) + using (var br = new BinaryReader(ms)) + { + int count = br.ReadInt32(); + for (int i = 0; i < count; i++) + { + int teamId = br.ReadInt32(); + Guid guid = new Guid(br.ReadBytes(16)); + + string uniqueName = br.ReadString(); + string customName = br.ReadString(); + + Vector3 globalPosition = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + Quaternion rotation = new Quaternion(br.ReadSingle(), br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + Vector3 localPosition = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + + bool built = br.ReadBoolean(); + bool placed = br.ReadBoolean(); + bool open = br.ReadBoolean(); + bool doBuildAnimation = br.ReadBoolean(); + bool constructionPaused = br.ReadBoolean(); + float constructionProgress = br.ReadSingle(); + float resourceProgress = br.ReadSingle(); + float life = br.ReadSingle(); + float modifiedMaxLife = br.ReadSingle(); + int yearBuilt = br.ReadInt32(); + float decayProtection = br.ReadSingle(); + + ApplyBuildingRecord(teamId, guid, uniqueName, customName, globalPosition, rotation, localPosition, built, placed, open, doBuildAnimation, constructionPaused, constructionProgress, resourceProgress, life, modifiedMaxLife, yearBuilt, decayProtection); + } + } + + TryRefreshFieldSystem(); + } + + private static void ApplyBuildingRecord(int teamId, Guid guid, string uniqueName, string customName, Vector3 globalPosition, Quaternion rotation, Vector3 localPosition, bool built, bool placed, bool open, bool doBuildAnimation, bool constructionPaused, float constructionProgress, float resourceProgress, float life, float modifiedMaxLife, int yearBuilt, float decayProtection) + { + Player p = Main.GetPlayerByTeamID(teamId); + if (p == null) + return; + + Building building = null; + try { building = p.GetBuilding(guid); } catch { } + if (building == null) + return; + + try + { + building.UniqueName = uniqueName; + building.customName = customName; + + building.transform.position = globalPosition; + if (building.transform.childCount > 0) + { + building.transform.GetChild(0).rotation = rotation; + building.transform.GetChild(0).localPosition = localPosition; + } + else + { + building.transform.rotation = rotation; + } + + SetPrivateFieldValue(building, "built", built); + SetPrivateFieldValue(building, "placed", placed); + SetPrivateFieldValue(building, "resourceProgress", resourceProgress); + SetPrivateFieldValue(building, "yearBuilt", yearBuilt); + + building.Open = open; + building.doBuildAnimation = doBuildAnimation; + building.constructionPaused = constructionPaused; + building.constructionProgress = constructionProgress; + building.Life = life; + building.ModifiedMaxLife = modifiedMaxLife; + building.decayProtection = decayProtection; + } + catch + { + } + } + + private static void SetPrivateFieldValue(object obj, string fieldName, object value) + { + if (obj == null) + return; + + Type type = obj.GetType(); + FieldInfo field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null) + field.SetValue(obj, value); + } + + private static void SendVillagerTeleportSnapshotToClient(ushort toClient) + { + try + { + int sent = 0; + for (int i = 0; i < Villager.villagers.Count; i++) + { + if (sent >= MaxVillagerTeleportsPerResync) + break; + + Villager v = Villager.villagers.data[i]; + if (v == null) + continue; + + new VillagerTeleportTo + { + guid = v.guid, + pos = v.Pos + }.Send(toClient); + + sent++; + } + } + catch + { + } + } + + public static void ApplyResourceSnapshot(List resourceTypes, List amounts) + { + if (resourceTypes == null || amounts == null) + return; + + int count = Math.Min(resourceTypes.Count, amounts.Count); + if (count == 0) + return; + + for (int i = 0; i < count; i++) + { + try + { + FreeResourceType type = (FreeResourceType)resourceTypes[i]; + int amount = amounts[i]; + TryWriteFreeResource(type, amount); + } + catch + { + } + } + + TryRefreshFreeResourceUI(); + } + + private static bool TryReadFreeResources(out List types, out List amounts) + { + types = new List(); + amounts = new List(); + + if (FreeResourceManager.inst == null) + return false; + + try + { + Array values = Enum.GetValues(typeof(FreeResourceType)); + foreach (var v in values) + { + FreeResourceType t = (FreeResourceType)v; + int amount; + if (!TryReadFreeResource(t, out amount)) + continue; + + types.Add((int)t); + amounts.Add(amount); + } + } + catch + { + return false; + } + + return types.Count > 0; + } + + private static bool EnsureResourceReflection() + { + if (resourceAmountGetMethod != null && resourceAmountSetMethod != null && freeResourceAmountField != null) + return true; + + try + { + Type raType = typeof(ResourceAmount); + resourceAmountGetMethod = raType.GetMethod("Get", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(FreeResourceType) }, null); + resourceAmountSetMethod = raType.GetMethod("Set", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(FreeResourceType), typeof(int) }, null); + + var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + freeResourceAmountField = typeof(FreeResourceManager).GetFields(bindingFlags).FirstOrDefault(f => f.FieldType == raType); + if (freeResourceAmountField == null) + { + var prop = typeof(FreeResourceManager).GetProperties(bindingFlags).FirstOrDefault(p => p.PropertyType == raType && p.GetGetMethod(true) != null); + if (prop != null) + { + // Fallback: treat property getter as "field" by caching getter only. + // We won't be able to set back reliably in this case. + } + } + + freeResourceManagerMaybeRefresh = typeof(FreeResourceManager).GetMethods(bindingFlags) + .FirstOrDefault(m => m.GetParameters().Length == 0 && m.ReturnType == typeof(void) && (m.Name.IndexOf("Refresh", StringComparison.OrdinalIgnoreCase) >= 0 || m.Name.IndexOf("Update", StringComparison.OrdinalIgnoreCase) >= 0)); + } + catch + { + return false; + } + + return freeResourceAmountField != null && resourceAmountGetMethod != null && resourceAmountSetMethod != null; + } + + private static bool TryReadFreeResource(FreeResourceType type, out int amount) + { + amount = 0; + + if (!EnsureResourceReflection()) + return false; + + try + { + object ra = freeResourceAmountField.GetValue(FreeResourceManager.inst); + if (ra == null) + return false; + + object result = resourceAmountGetMethod.Invoke(ra, new object[] { type }); + if (result is int) + { + amount = (int)result; + return true; + } + + return false; + } + catch + { + return false; + } + } + + private static bool TryWriteFreeResource(FreeResourceType type, int amount) + { + if (!EnsureResourceReflection()) + return false; + + try + { + object ra = freeResourceAmountField.GetValue(FreeResourceManager.inst); + if (ra == null) + return false; + + resourceAmountSetMethod.Invoke(ra, new object[] { type, amount }); + if (typeof(ResourceAmount).IsValueType) + freeResourceAmountField.SetValue(FreeResourceManager.inst, ra); + + return true; + } + catch + { + return false; + } + } + + private static void TryRefreshFreeResourceUI() + { + try + { + if (!EnsureResourceReflection()) + return; + + if (freeResourceManagerMaybeRefresh != null && FreeResourceManager.inst != null) + freeResourceManagerMaybeRefresh.Invoke(FreeResourceManager.inst, null); + } + catch + { + } + } + + private static void TryRefreshFieldSystem() + { + try + { + if (Player.inst == null || Player.inst.fieldSystem == null) + return; + + if (fieldSystemMaybeRefresh == null) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + fieldSystemMaybeRefresh = Player.inst.fieldSystem.GetType() + .GetMethods(bindingFlags) + .FirstOrDefault(m => + m.ReturnType == typeof(void) && + m.GetParameters().Length == 0 && + (m.Name.IndexOf("Rebuild", StringComparison.OrdinalIgnoreCase) >= 0 || + m.Name.IndexOf("Refresh", StringComparison.OrdinalIgnoreCase) >= 0) && + m.Name.IndexOf("Reset", StringComparison.OrdinalIgnoreCase) < 0 && + m.Name.IndexOf("Clear", StringComparison.OrdinalIgnoreCase) < 0); + } + + if (fieldSystemMaybeRefresh != null) + fieldSystemMaybeRefresh.Invoke(Player.inst.fieldSystem, null); + } + catch + { + } + } + } +}