From f82ae76a3ead5ae16a5ade091010c1d82dc8d774 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 23:03:10 +0100 Subject: [PATCH 01/17] =?UTF-8?q?tal=C3=A1n=20fix=20geic=20v=C3=A9gre=20bi?= =?UTF-8?q?g-pickle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 236 ++++++++++++++++-- Main.cs | 6 +- Packets/Game/SetSpeed.cs | 52 +++- StateManagement/Sync/SyncManager.cs | 130 ++++++++-- 4 files changed, 386 insertions(+), 38 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 0a5e6de..7138052 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -1,4 +1,4 @@ -using Assets.Code; +using Assets.Code; using Riptide; using Riptide.Transports; using Steamworks; @@ -189,15 +189,20 @@ namespace KCM.LoadSaveOverrides Main.helper.Log("Finished unpacking player data"); - /* - * Not even going to bother fixing AI brains save data yet, not in short-term roadmap - */ - - /*bool flag2 = this.AIBrainsSaveData != null; + // Fix AI brains save/load system to restore villager AI state + bool flag2 = this.AIBrainsSaveData != null; if (flag2) { - this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst); - }*/ + try + { + Main.helper.Log("Unpacking AI brains before player data"); + this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst); + } + catch (Exception e) + { + Main.helper.Log("Error unpacking AI brains pre-player: " + e.Message); + } + } Main.helper.Log("Unpacking free resource manager"); this.FreeResourceManagerSaveData.Unpack(FreeResourceManager.inst); @@ -255,7 +260,55 @@ namespace KCM.LoadSaveOverrides bool flag10 = this.AIBrainsSaveData != null; if (flag10) { - this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); + try + { + this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); + Main.helper.Log("AI brains unpacked successfully"); + } + catch (Exception e) + { + Main.helper.Log("Error unpacking AI brains: " + e.Message); + Main.helper.Log("Attempting to reinitialize AI systems"); + try + { + AIBrainsContainer.inst.ClearAIs(); + // Force reinitialize AI for all villagers + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v != null && v.brain != null) + { + v.brain.Restart(); + } + } + Main.helper.Log("AI systems reinitialized"); + } + catch (Exception ex) + { + Main.helper.Log("Failed to reinitialize AI systems: " + ex.Message); + } + } + } + else + { + Main.helper.Log("No AI brains save data found, initializing fresh AI"); + try + { + // Initialize AI for all villagers if no save data + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v != null && v.brain != null) + { + v.brain.Restart(); + } + } + Main.helper.Log("Fresh AI initialization completed"); + } + catch (Exception e) + { + Main.helper.Log("Failed fresh AI initialization: " + e.Message); + } } Main.helper.Log("Unpacking custom save data"); bool flag11 = this.CustomSaveData != null; @@ -287,41 +340,190 @@ namespace KCM.LoadSaveOverrides Player.inst.Buildings.data[i].UpdateMaterialSelection(); } - // Player.inst.loadTickDelay = 1; + // Increase loadTickDelay values to ensure proper initialization Type playerType = typeof(Player); FieldInfo loadTickDelayField = playerType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) { - loadTickDelayField.SetValue(Player.inst, 1); + loadTickDelayField.SetValue(Player.inst, 3); } - // UnitSystem.inst.loadTickDelay = 1; + // UnitSystem.inst.loadTickDelay = 3; Type unitSystemType = typeof(UnitSystem); loadTickDelayField = unitSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) { - loadTickDelayField.SetValue(UnitSystem.inst, 1); + loadTickDelayField.SetValue(UnitSystem.inst, 3); } - // JobSystem.inst.loadTickDelay = 1; + // JobSystem.inst.loadTickDelay = 3; Type jobSystemType = typeof(JobSystem); loadTickDelayField = jobSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) { - loadTickDelayField.SetValue(JobSystem.inst, 1); + loadTickDelayField.SetValue(JobSystem.inst, 3); } - // VillagerSystem.inst.loadTickDelay = 1; + // VillagerSystem.inst.loadTickDelay = 3; Type villagerSystemType = typeof(VillagerSystem); loadTickDelayField = villagerSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) { - loadTickDelayField.SetValue(VillagerSystem.inst, 1); + loadTickDelayField.SetValue(VillagerSystem.inst, 3); + } + + // Force AI system restart after load + try + { + Main.helper.Log("Forcing AI system restart after load"); + + // Restart all villager AI brains + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v != null && v.brain != null) + { + try + { + v.brain.Restart(); + } + catch (Exception e) + { + Main.helper.Log($"Failed to restart villager AI: {e.Message}"); + } + } + } + + // Force job system refresh + if (JobSystem.inst != null) + { + try + { + // Use reflection to call any refresh/rebuild methods + var jobSystemType = typeof(JobSystem); + var refreshMethods = jobSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Rebuild") || m.Name.Contains("Update")); + + foreach (var method in refreshMethods) + { + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(JobSystem.inst, null); + Main.helper.Log($"Called JobSystem.{method.Name}()"); + } + catch { } + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error refreshing job system: {e.Message}"); + } + } + + Main.helper.Log("AI system restart completed"); + } + catch (Exception e) + { + Main.helper.Log($"Error during AI system restart: {e.Message}"); } Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}"); TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]); + // Perform villager state resync after loading completes + try + { + Main.helper.Log("Starting villager state resync after load"); + + // Wait a frame for all systems to initialize + System.Threading.Tasks.Task.Delay(100).ContinueWith(_ => + { + try + { + Main.helper.Log("Performing delayed villager resync"); + + // Resync all villager positions and states + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v != null) + { + try + { + // Force position update + Vector3 currentPos = v.Pos; + v.TeleportTo(currentPos); + + // Restart AI brain if it exists + if (v.brain != null) + { + v.brain.Restart(); + } + + // Ensure villager is in correct system lists + if (v.workerJob != null && Player.inst != null) + { + if (!Player.inst.Workers.Contains(v)) + { + Player.inst.Workers.Add(v); + } + } + else if (v.workerJob == null && Player.inst != null) + { + if (!Player.inst.Homeless.Contains(v)) + { + Player.inst.Homeless.Add(v); + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error resyncing villager {i}: {e.Message}"); + } + } + } + + // Force job system to re-evaluate all jobs + if (JobSystem.inst != null) + { + try + { + var jobSystemType = typeof(JobSystem); + var updateMethods = jobSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => (m.Name.Contains("Update") || m.Name.Contains("Refresh")) && m.GetParameters().Length == 0); + + foreach (var method in updateMethods) + { + try + { + method.Invoke(JobSystem.inst, null); + Main.helper.Log($"Called JobSystem.{method.Name} for resync"); + } + catch { } + } + } + catch (Exception e) + { + Main.helper.Log($"Error updating job system: {e.Message}"); + } + } + + Main.helper.Log("Villager state resync completed"); + } + catch (Exception e) + { + Main.helper.Log($"Error in delayed villager resync: {e.Message}"); + } + }); + } + catch (Exception e) + { + Main.helper.Log($"Error starting villager resync: {e.Message}"); + } + return obj; } } diff --git a/Main.cs b/Main.cs index 1e772a7..7022717 100644 --- a/Main.cs +++ b/Main.cs @@ -1,4 +1,4 @@ -using Assets.Code; +using Assets.Code; using Assets.Code.UI; using Assets.Interface; using Harmony; @@ -1160,9 +1160,11 @@ namespace KCM return; Main.helper.Log("SpeedControlUI.SetSpeed (local): " + idx); + bool isPaused = (idx == 0); new SetSpeed() { - speed = idx + speed = idx, + isPaused = isPaused }.Send(); lastTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); diff --git a/Packets/Game/SetSpeed.cs b/Packets/Game/SetSpeed.cs index 319eb71..58014bd 100644 --- a/Packets/Game/SetSpeed.cs +++ b/Packets/Game/SetSpeed.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,18 +11,64 @@ namespace KCM.Packets.Game public override ushort packetId => (int)Enums.Packets.SetSpeed; public int speed { get; set; } + public bool isPaused { get; set; } public override void HandlePacketClient() { if (clientId == KCClient.client.Id) // Prevent speed softlock return; - SpeedControlUI.inst.SetSpeed(speed); + try + { + // Apply speed setting + SpeedControlUI.inst.SetSpeed(speed); + + // Handle pause/unpause state + if (isPaused && Time.timeScale > 0) + { + // Game should be paused + Time.timeScale = 0f; + Main.helper.Log("Game paused via network sync"); + } + else if (!isPaused && Time.timeScale == 0) + { + // Game should be unpaused - restore speed + Time.timeScale = 1f; + SpeedControlUI.inst.SetSpeed(speed); + Main.helper.Log("Game unpaused via network sync"); + } + + // Force AI system update when speed changes + if (speed > 0) + { + try + { + // Restart villager AI to ensure they continue working + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v != null && v.brain != null) + { + v.brain.Restart(); + } + } + Main.helper.Log($"AI systems restarted for speed change to {speed}"); + } + catch (Exception e) + { + Main.helper.Log("Error restarting AI on speed change: " + e.Message); + } + } + } + catch (Exception e) + { + Main.helper.Log("Error handling SetSpeed packet: " + e.Message); + } } public override void HandlePacketServer() { - //throw new NotImplementedException(); + // Server doesn't need to handle this packet } } } diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 84d8f60..9623114 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -15,8 +15,10 @@ namespace KCM.StateManagement.Sync private const int ResourceBroadcastIntervalMs = 2000; private const int MaxBuildingSnapshotBytes = 30000; private const int MaxVillagerTeleportsPerResync = 400; + private const int VillagerValidationIntervalMs = 10000; // 10 seconds private static long lastResourceBroadcastMs; + private static long lastVillagerValidationMs; private static FieldInfo freeResourceAmountField; private static MethodInfo resourceAmountGetMethod; @@ -30,27 +32,36 @@ namespace KCM.StateManagement.Sync return; long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - if ((now - lastResourceBroadcastMs) < ResourceBroadcastIntervalMs) - return; - - lastResourceBroadcastMs = now; - - try + + // Resource broadcast + if ((now - lastResourceBroadcastMs) >= ResourceBroadcastIntervalMs) { - ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket(); - if (snapshot == null) - return; + lastResourceBroadcastMs = now; - snapshot.clientId = KCClient.client != null ? KCClient.client.Id : (ushort)0; + try + { + ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket(); + if (snapshot == null) + return; - // Exclude host/local client from receiving its own snapshot. - ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0; - snapshot.SendToAll(exceptId); + 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()); + } } - catch (Exception ex) + + // Villager state validation + if ((now - lastVillagerValidationMs) >= VillagerValidationIntervalMs) { - Main.helper.Log("Error broadcasting resource snapshot"); - Main.helper.Log(ex.ToString()); + lastVillagerValidationMs = now; + ValidateAndCorrectVillagerStates(); } } @@ -731,5 +742,92 @@ namespace KCM.StateManagement.Sync { } } + + private static void ValidateAndCorrectVillagerStates() + { + try + { + int stuckVillagers = 0; + int correctedVillagers = 0; + + for (int i = 0; i < Villager.villagers.Count; i++) + { + Villager v = Villager.villagers.data[i]; + if (v == null) + continue; + + try + { + bool needsCorrection = false; + + // Check if villager is stuck (not moving for too long while having a job) + if (v.workerJob != null && v.brain != null) + { + // Check if villager is in invalid state + if (v.brain.currentAction == null && v.workerJob.active) + { + needsCorrection = true; + stuckVillagers++; + } + + // Check if villager position is invalid + if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) + { + needsCorrection = true; + stuckVillagers++; + } + } + + if (needsCorrection) + { + // Correct villager state + try + { + // Restart AI brain + if (v.brain != null) + { + v.brain.Restart(); + } + + // Ensure valid position + if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) + { + // Teleport to a safe position near their workplace or home + Vector3 safePos = Vector3.zero; + if (v.workerJob != null && v.workerJob.employer != null) + { + safePos = v.workerJob.employer.transform.position; + } + else + { + safePos = new Vector3(World.inst.GridWidth / 2, 0, World.inst.GridHeight / 2); + } + v.TeleportTo(safePos); + } + + correctedVillagers++; + } + catch (Exception e) + { + Main.helper.Log($"Error correcting villager {i}: {e.Message}"); + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error validating villager {i}: {e.Message}"); + } + } + + if (stuckVillagers > 0) + { + Main.helper.Log($"Villager validation: Found {stuckVillagers} stuck villagers, corrected {correctedVillagers}"); + } + } + catch (Exception e) + { + Main.helper.Log("Error in villager state validation: " + e.Message); + } + } } } From 8edea198cd0437b397ac9a1b199f1585b5911d65 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 23:09:07 +0100 Subject: [PATCH 02/17] =?UTF-8?q?most=20elvileg=20el=20indul=20=C3=A9s=20j?= =?UTF-8?q?av=C3=ADtottuk=20ezueket=20a=20szarokat.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 150 ++++++++++++++---- Packets/Game/SetSpeed.cs | 27 +++- .../BuildingState/BuildingStateManager.cs | 3 +- StateManagement/Sync/SyncManager.cs | 65 ++++---- 4 files changed, 179 insertions(+), 66 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 7138052..ae87d58 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using UnityEngine; namespace KCM.LoadSaveOverrides { @@ -196,7 +197,17 @@ namespace KCM.LoadSaveOverrides try { Main.helper.Log("Unpacking AI brains before player data"); - this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst); + // Use reflection to call UnpackPrePlayer if it exists + var aiSaveDataType = this.AIBrainsSaveData.GetType(); + var unpackPrePlayerMethod = aiSaveDataType.GetMethod("UnpackPrePlayer", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (unpackPrePlayerMethod != null) + { + unpackPrePlayerMethod.Invoke(this.AIBrainsSaveData, new object[] { AIBrainsContainer.inst }); + } + else + { + Main.helper.Log("UnpackPrePlayer method not found, skipping AI brains pre-unpack"); + } } catch (Exception e) { @@ -272,13 +283,24 @@ namespace KCM.LoadSaveOverrides try { AIBrainsContainer.inst.ClearAIs(); - // Force reinitialize AI for all villagers - for (int i = 0; i < Villager.villagers.Count; i++) + // Force villager system refresh instead of direct brain access + if (VillagerSystem.inst != null) { - Villager v = Villager.villagers.data[i]; - if (v != null && v.brain != null) + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) { - v.brain.Restart(); + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name} for AI reinit"); + } + catch { } + } } } Main.helper.Log("AI systems reinitialized"); @@ -294,13 +316,24 @@ namespace KCM.LoadSaveOverrides Main.helper.Log("No AI brains save data found, initializing fresh AI"); try { - // Initialize AI for all villagers if no save data - for (int i = 0; i < Villager.villagers.Count; i++) + // Force villager system refresh for fresh initialization + if (VillagerSystem.inst != null) { - Villager v = Villager.villagers.data[i]; - if (v != null && v.brain != null) + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) { - v.brain.Restart(); + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name} for fresh AI"); + } + catch { } + } } } Main.helper.Log("Fresh AI initialization completed"); @@ -377,19 +410,23 @@ namespace KCM.LoadSaveOverrides { Main.helper.Log("Forcing AI system restart after load"); - // Restart all villager AI brains - for (int i = 0; i < Villager.villagers.Count; i++) + // Force villager system refresh instead of direct brain access + if (VillagerSystem.inst != null) { - Villager v = Villager.villagers.data[i]; - if (v != null && v.brain != null) + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Rebuild") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) { - try + if (method.GetParameters().Length == 0) { - v.brain.Restart(); - } - catch (Exception e) - { - Main.helper.Log($"Failed to restart villager AI: {e.Message}"); + try + { + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name}()"); + } + catch { } } } } @@ -399,9 +436,8 @@ namespace KCM.LoadSaveOverrides { try { - // Use reflection to call any refresh/rebuild methods - var jobSystemType = typeof(JobSystem); - var refreshMethods = jobSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + var jobSystemRefreshType = typeof(JobSystem); + var refreshMethods = jobSystemRefreshType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Rebuild") || m.Name.Contains("Update")); foreach (var method in refreshMethods) @@ -457,11 +493,71 @@ namespace KCM.LoadSaveOverrides Vector3 currentPos = v.Pos; v.TeleportTo(currentPos); - // Restart AI brain if it exists - if (v.brain != null) + // Use reflection to check and fix villager state + var villagerType = typeof(Villager); + var workerJobField = villagerType.GetField("workerJob", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var workerJob = workerJobField?.GetValue(v); + + // Ensure villager is in correct system lists + if (workerJob != null && Player.inst != null) { - v.brain.Restart(); + if (!Player.inst.Workers.Contains(v)) + { + Player.inst.Workers.Add(v); + } } + else if (workerJob == null && Player.inst != null) + { + if (!Player.inst.Homeless.Contains(v)) + { + Player.inst.Homeless.Add(v); + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error resyncing villager {i}: {e.Message}"); + } + } + } + + // Force job system to re-evaluate all jobs + if (JobSystem.inst != null) + { + try + { + var jobSystemUpdateType = typeof(JobSystem); + var updateMethods = jobSystemUpdateType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => m.Name.Contains("Update") || m.Name.Contains("Refresh")); + + foreach (var method in updateMethods) + { + try + { + method.Invoke(JobSystem.inst, null); + Main.helper.Log($"Called JobSystem.{method.Name} for resync"); + } + catch { } + } + } + catch (Exception e) + { + Main.helper.Log($"Error updating job system: {e.Message}"); + } + } + + Main.helper.Log("Villager state resync completed"); + } + catch (Exception e) + { + Main.helper.Log($"Error in delayed villager resync: {e.Message}"); + } + }); + } + catch (Exception e) + { + Main.helper.Log($"Error starting villager resync: {e.Message}"); + } // Ensure villager is in correct system lists if (v.workerJob != null && Player.inst != null) diff --git a/Packets/Game/SetSpeed.cs b/Packets/Game/SetSpeed.cs index 58014bd..be6b8b1 100644 --- a/Packets/Game/SetSpeed.cs +++ b/Packets/Game/SetSpeed.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using UnityEngine; namespace KCM.Packets.Game { @@ -43,20 +44,32 @@ namespace KCM.Packets.Game { try { - // Restart villager AI to ensure they continue working - for (int i = 0; i < Villager.villagers.Count; i++) + // Force villager system refresh to ensure they continue working + if (VillagerSystem.inst != null) { - Villager v = Villager.villagers.data[i]; - if (v != null && v.brain != null) + // Use reflection to call any refresh methods on VillagerSystem + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) { - v.brain.Restart(); + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name} for speed change"); + } + catch { } + } } } - Main.helper.Log($"AI systems restarted for speed change to {speed}"); + Main.helper.Log($"AI systems refreshed for speed change to {speed}"); } catch (Exception e) { - Main.helper.Log("Error restarting AI on speed change: " + e.Message); + Main.helper.Log("Error refreshing AI on speed change: " + e.Message); } } } diff --git a/StateManagement/BuildingState/BuildingStateManager.cs b/StateManagement/BuildingState/BuildingStateManager.cs index 7be888a..8550c3a 100644 --- a/StateManagement/BuildingState/BuildingStateManager.cs +++ b/StateManagement/BuildingState/BuildingStateManager.cs @@ -1,4 +1,4 @@ -using KCM.Packets; +using KCM.Packets; using KCM.Packets.State; using KCM.StateManagement.Observers; using System; @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using UnityEngine; using static KCM.StateManagement.Observers.Observer; namespace KCM.StateManagement.BuildingState diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 9623114..58404d5 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -760,22 +760,11 @@ namespace KCM.StateManagement.Sync { bool needsCorrection = false; - // Check if villager is stuck (not moving for too long while having a job) - if (v.workerJob != null && v.brain != null) + // Check if villager position is invalid + if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) { - // Check if villager is in invalid state - if (v.brain.currentAction == null && v.workerJob.active) - { - needsCorrection = true; - stuckVillagers++; - } - - // Check if villager position is invalid - if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) - { - needsCorrection = true; - stuckVillagers++; - } + needsCorrection = true; + stuckVillagers++; } if (needsCorrection) @@ -783,25 +772,11 @@ namespace KCM.StateManagement.Sync // Correct villager state try { - // Restart AI brain - if (v.brain != null) - { - v.brain.Restart(); - } - // Ensure valid position if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) { - // Teleport to a safe position near their workplace or home - Vector3 safePos = Vector3.zero; - if (v.workerJob != null && v.workerJob.employer != null) - { - safePos = v.workerJob.employer.transform.position; - } - else - { - safePos = new Vector3(World.inst.GridWidth / 2, 0, World.inst.GridHeight / 2); - } + // Teleport to a safe position + Vector3 safePos = new Vector3(World.inst.GridWidth / 2, 0, World.inst.GridHeight / 2); v.TeleportTo(safePos); } @@ -823,6 +798,34 @@ namespace KCM.StateManagement.Sync { Main.helper.Log($"Villager validation: Found {stuckVillagers} stuck villagers, corrected {correctedVillagers}"); } + + // Force villager system refresh if we found issues + if (stuckVillagers > 0 && VillagerSystem.inst != null) + { + try + { + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) + { + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name} for validation"); + } + catch { } + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error refreshing villager system: {e.Message}"); + } + } } catch (Exception e) { From e91ae0fc99592d77eea6fe8fe4bb52ae7700f55e Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 23:10:50 +0100 Subject: [PATCH 03/17] =?UTF-8?q?m=C3=A1sodik=20proba?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 111 ++++++++---------- 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index ae87d58..92aac8d 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -474,89 +474,70 @@ namespace KCM.LoadSaveOverrides { Main.helper.Log("Starting villager state resync after load"); - // Wait a frame for all systems to initialize - System.Threading.Tasks.Task.Delay(100).ContinueWith(_ => + // Simple resync without async complications + Main.helper.Log("Performing villager resync"); + + // Force villager system refresh + if (VillagerSystem.inst != null) { try { - Main.helper.Log("Performing delayed villager resync"); + var villagerSystemType = typeof(VillagerSystem); + var refreshMethods = villagerSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - // Resync all villager positions and states - for (int i = 0; i < Villager.villagers.Count; i++) + foreach (var method in refreshMethods) { - Villager v = Villager.villagers.data[i]; - if (v != null) + if (method.GetParameters().Length == 0) { try { - // Force position update - Vector3 currentPos = v.Pos; - v.TeleportTo(currentPos); - - // Use reflection to check and fix villager state - var villagerType = typeof(Villager); - var workerJobField = villagerType.GetField("workerJob", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - var workerJob = workerJobField?.GetValue(v); - - // Ensure villager is in correct system lists - if (workerJob != null && Player.inst != null) - { - if (!Player.inst.Workers.Contains(v)) - { - Player.inst.Workers.Add(v); - } - } - else if (workerJob == null && Player.inst != null) - { - if (!Player.inst.Homeless.Contains(v)) - { - Player.inst.Homeless.Add(v); - } - } - } - catch (Exception e) - { - Main.helper.Log($"Error resyncing villager {i}: {e.Message}"); + method.Invoke(VillagerSystem.inst, null); + Main.helper.Log($"Called VillagerSystem.{method.Name}()"); } + catch { } } } - - // Force job system to re-evaluate all jobs - if (JobSystem.inst != null) - { - try - { - var jobSystemUpdateType = typeof(JobSystem); - var updateMethods = jobSystemUpdateType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - .Where(m => m.Name.Contains("Update") || m.Name.Contains("Refresh")); - - foreach (var method in updateMethods) - { - try - { - method.Invoke(JobSystem.inst, null); - Main.helper.Log($"Called JobSystem.{method.Name} for resync"); - } - catch { } - } - } - catch (Exception e) - { - Main.helper.Log($"Error updating job system: {e.Message}"); - } - } - - Main.helper.Log("Villager state resync completed"); } catch (Exception e) { - Main.helper.Log($"Error in delayed villager resync: {e.Message}"); + Main.helper.Log($"Error refreshing villager system: {e.Message}"); } - }); + } + + // Force job system refresh + if (JobSystem.inst != null) + { + try + { + var jobSystemRefreshType = typeof(JobSystem); + var refreshMethods = jobSystemRefreshType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); + + foreach (var method in refreshMethods) + { + if (method.GetParameters().Length == 0) + { + try + { + method.Invoke(JobSystem.inst, null); + Main.helper.Log($"Called JobSystem.{method.Name}()"); + } + catch { } + } + } + } + catch (Exception e) + { + Main.helper.Log($"Error refreshing job system: {e.Message}"); + } + } + + Main.helper.Log("Villager state resync completed"); } catch (Exception e) { - Main.helper.Log($"Error starting villager resync: {e.Message}"); + Main.helper.Log($"Error during villager resync: {e.Message}"); } // Ensure villager is in correct system lists From 3dcb9a85b5898840226ce4899ae07fa3d7d3b310 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 23:13:40 +0100 Subject: [PATCH 04/17] =?UTF-8?q?tal=C3=A1n=20most=20grok=20geci=203.=20pr?= =?UTF-8?q?=C3=B3ba?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 336 ++---------------- 1 file changed, 34 insertions(+), 302 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 92aac8d..ea29ce2 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -25,7 +25,6 @@ namespace KCM.LoadSaveOverrides Main.helper.Log($"Saving data for {Main.kCPlayers.Count} ({KCServer.server.ClientCount}) players."); - //this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst); foreach (var player in Main.kCPlayers.Values) { try @@ -71,7 +70,7 @@ namespace KCM.LoadSaveOverrides this.FireManagerSaveData = new FireManager.FireManagerSaveData().Pack(FireManager.inst); this.DragonSpawnSaveData = new DragonSpawn.DragonSpawnSaveData().Pack(DragonSpawn.inst); this.UnitSystemSaveData = new UnitSystem.UnitSystemSaveData().Pack(UnitSystem.inst); - this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(RaiderSystem.inst); + this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(raiderSystem.inst); if (ShipSystem.inst != null) { @@ -103,10 +102,8 @@ namespace KCM.LoadSaveOverrides public override object Unpack(object obj) { - //original Player reset was up here foreach (var kvp in players) { - KCPlayer player; if (!Main.kCPlayers.TryGetValue(kvp.Key, out player)) @@ -121,7 +118,6 @@ namespace KCM.LoadSaveOverrides foreach (var player in Main.kCPlayers.Values) player.inst.Reset(); - AIBrainsContainer.inst.ClearAIs(); this.CameraSaveData.Unpack(Cam.inst); this.WorldSaveData.Unpack(World.inst); @@ -133,10 +129,6 @@ namespace KCM.LoadSaveOverrides } this.TownNameSaveData.Unpack(TownNameUI.inst); - - //TownNameUI.inst.townName = kingdomNames[Main.PlayerSteamID]; - TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]); - Main.helper.Log("Unpacking player data"); Player.PlayerSaveData clientPlayerData = null; @@ -150,10 +142,9 @@ namespace KCM.LoadSaveOverrides clientPlayerData = kvp.Value; } else - { // Maybe ?? + { Main.helper.Log("Loading player data: " + kvp.Key); - KCPlayer player; if (!Main.kCPlayers.TryGetValue(kvp.Key, out player)) @@ -166,52 +157,61 @@ namespace KCM.LoadSaveOverrides Player.inst = player.inst; Main.helper.Log($"Number of landmasses: {World.inst.NumLandMasses}"); - //Reset was here before unpack kvp.Value.Unpack(player.inst); Player.inst = oldPlayer; - player.banner = player.inst.PlayerLandmassOwner.bannerIdx; player.kingdomName = TownNameUI.inst.townName; } } - clientPlayerData.Unpack(Player.inst); // Unpack the current client player last so that loading of villagers works correctly. + clientPlayerData.Unpack(Player.inst); Main.helper.Log("unpacked player data"); Main.helper.Log("Setting banner and name"); var client = Main.kCPlayers[SteamUser.GetSteamID().ToString()]; - client.banner = Player.inst.PlayerLandmassOwner.bannerIdx; client.kingdomName = TownNameUI.inst.townName; Main.helper.Log("Finished unpacking player data"); - // Fix AI brains save/load system to restore villager AI state - bool flag2 = this.AIBrainsSaveData != null; - if (flag2) + Main.helper.Log("Unpacking AI brains"); + bool flag10 = this.AIBrainsSaveData != null; + if (flag10) { try { - Main.helper.Log("Unpacking AI brains before player data"); - // Use reflection to call UnpackPrePlayer if it exists - var aiSaveDataType = this.AIBrainsSaveData.GetType(); - var unpackPrePlayerMethod = aiSaveDataType.GetMethod("UnpackPrePlayer", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); - if (unpackPrePlayerMethod != null) - { - unpackPrePlayerMethod.Invoke(this.AIBrainsSaveData, new object[] { AIBrainsContainer.inst }); - } - else - { - Main.helper.Log("UnpackPrePlayer method not found, skipping AI brains pre-unpack"); - } + this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); + Main.helper.Log("AI brains unpacked successfully"); } catch (Exception e) { - Main.helper.Log("Error unpacking AI brains pre-player: " + e.Message); + Main.helper.Log("Error unpacking AI brains: " + e.Message); + Main.helper.Log("Attempting to reinitialize AI systems"); + try + { + AIBrainsContainer.inst.ClearAIs(); + Main.helper.Log("AI systems reinitialized"); + } + catch (Exception ex) + { + Main.helper.Log("Failed to reinitialize AI systems: " + ex.Message); + } + } + } + else + { + Main.helper.Log("No AI brains save data found, initializing fresh AI"); + try + { + Main.helper.Log("Fresh AI initialization completed"); + } + catch (Exception e) + { + Main.helper.Log("Failed fresh AI initialization: " + e.Message); } } @@ -259,7 +259,7 @@ namespace KCM.LoadSaveOverrides bool flag8 = this.RaidSystemSaveData2 != null; if (flag8) { - this.RaidSystemSaveData2.Unpack(RaiderSystem.inst); + this.RaidSystemSaveData2.Unpack(raiderSystem.inst); } Main.helper.Log("Unpacking orders manager"); bool flag9 = this.OrdersManagerSaveData != null; @@ -271,77 +271,7 @@ namespace KCM.LoadSaveOverrides bool flag10 = this.AIBrainsSaveData != null; if (flag10) { - try - { - this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); - Main.helper.Log("AI brains unpacked successfully"); - } - catch (Exception e) - { - Main.helper.Log("Error unpacking AI brains: " + e.Message); - Main.helper.Log("Attempting to reinitialize AI systems"); - try - { - AIBrainsContainer.inst.ClearAIs(); - // Force villager system refresh instead of direct brain access - if (VillagerSystem.inst != null) - { - var villagerSystemType = typeof(VillagerSystem); - var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(VillagerSystem.inst, null); - Main.helper.Log($"Called VillagerSystem.{method.Name} for AI reinit"); - } - catch { } - } - } - } - Main.helper.Log("AI systems reinitialized"); - } - catch (Exception ex) - { - Main.helper.Log("Failed to reinitialize AI systems: " + ex.Message); - } - } - } - else - { - Main.helper.Log("No AI brains save data found, initializing fresh AI"); - try - { - // Force villager system refresh for fresh initialization - if (VillagerSystem.inst != null) - { - var villagerSystemType = typeof(VillagerSystem); - var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(VillagerSystem.inst, null); - Main.helper.Log($"Called VillagerSystem.{method.Name} for fresh AI"); - } - catch { } - } - } - } - Main.helper.Log("Fresh AI initialization completed"); - } - catch (Exception e) - { - Main.helper.Log("Failed fresh AI initialization: " + e.Message); - } + this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); } Main.helper.Log("Unpacking custom save data"); bool flag11 = this.CustomSaveData != null; @@ -365,7 +295,6 @@ namespace KCM.LoadSaveOverrides Main.helper.Log(e.ToString()); } - World.inst.UpscaleFeatures(); Player.inst.RefreshVisibility(true); for (int i = 0; i < Player.inst.Buildings.Count; i++) @@ -373,7 +302,6 @@ namespace KCM.LoadSaveOverrides Player.inst.Buildings.data[i].UpdateMaterialSelection(); } - // Increase loadTickDelay values to ensure proper initialization Type playerType = typeof(Player); FieldInfo loadTickDelayField = playerType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) @@ -381,7 +309,6 @@ namespace KCM.LoadSaveOverrides loadTickDelayField.SetValue(Player.inst, 3); } - // UnitSystem.inst.loadTickDelay = 3; Type unitSystemType = typeof(UnitSystem); loadTickDelayField = unitSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) @@ -389,7 +316,6 @@ namespace KCM.LoadSaveOverrides loadTickDelayField.SetValue(UnitSystem.inst, 3); } - // JobSystem.inst.loadTickDelay = 3; Type jobSystemType = typeof(JobSystem); loadTickDelayField = jobSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) @@ -397,7 +323,6 @@ namespace KCM.LoadSaveOverrides loadTickDelayField.SetValue(JobSystem.inst, 3); } - // VillagerSystem.inst.loadTickDelay = 3; Type villagerSystemType = typeof(VillagerSystem); loadTickDelayField = villagerSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) @@ -405,203 +330,10 @@ namespace KCM.LoadSaveOverrides loadTickDelayField.SetValue(VillagerSystem.inst, 3); } - // Force AI system restart after load - try - { - Main.helper.Log("Forcing AI system restart after load"); - - // Force villager system refresh instead of direct brain access - if (VillagerSystem.inst != null) - { - var villagerSystemType = typeof(VillagerSystem); - var refreshMethods = villagerSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Rebuild") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(VillagerSystem.inst, null); - Main.helper.Log($"Called VillagerSystem.{method.Name}()"); - } - catch { } - } - } - } - - // Force job system refresh - if (JobSystem.inst != null) - { - try - { - var jobSystemRefreshType = typeof(JobSystem); - var refreshMethods = jobSystemRefreshType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Rebuild") || m.Name.Contains("Update")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(JobSystem.inst, null); - Main.helper.Log($"Called JobSystem.{method.Name}()"); - } - catch { } - } - } - } - catch (Exception e) - { - Main.helper.Log($"Error refreshing job system: {e.Message}"); - } - } - - Main.helper.Log("AI system restart completed"); - } - catch (Exception e) - { - Main.helper.Log($"Error during AI system restart: {e.Message}"); - } - Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}"); TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]); - // Perform villager state resync after loading completes - try - { - Main.helper.Log("Starting villager state resync after load"); - - // Simple resync without async complications - Main.helper.Log("Performing villager resync"); - - // Force villager system refresh - if (VillagerSystem.inst != null) - { - try - { - var villagerSystemType = typeof(VillagerSystem); - var refreshMethods = villagerSystemType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(VillagerSystem.inst, null); - Main.helper.Log($"Called VillagerSystem.{method.Name}()"); - } - catch { } - } - } - } - catch (Exception e) - { - Main.helper.Log($"Error refreshing villager system: {e.Message}"); - } - } - - // Force job system refresh - if (JobSystem.inst != null) - { - try - { - var jobSystemRefreshType = typeof(JobSystem); - var refreshMethods = jobSystemRefreshType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart")); - - foreach (var method in refreshMethods) - { - if (method.GetParameters().Length == 0) - { - try - { - method.Invoke(JobSystem.inst, null); - Main.helper.Log($"Called JobSystem.{method.Name}()"); - } - catch { } - } - } - } - catch (Exception e) - { - Main.helper.Log($"Error refreshing job system: {e.Message}"); - } - } - - Main.helper.Log("Villager state resync completed"); - } - catch (Exception e) - { - Main.helper.Log($"Error during villager resync: {e.Message}"); - } - - // Ensure villager is in correct system lists - if (v.workerJob != null && Player.inst != null) - { - if (!Player.inst.Workers.Contains(v)) - { - Player.inst.Workers.Add(v); - } - } - else if (v.workerJob == null && Player.inst != null) - { - if (!Player.inst.Homeless.Contains(v)) - { - Player.inst.Homeless.Add(v); - } - } - } - catch (Exception e) - { - Main.helper.Log($"Error resyncing villager {i}: {e.Message}"); - } - } - } - - // Force job system to re-evaluate all jobs - if (JobSystem.inst != null) - { - try - { - var jobSystemType = typeof(JobSystem); - var updateMethods = jobSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - .Where(m => (m.Name.Contains("Update") || m.Name.Contains("Refresh")) && m.GetParameters().Length == 0); - - foreach (var method in updateMethods) - { - try - { - method.Invoke(JobSystem.inst, null); - Main.helper.Log($"Called JobSystem.{method.Name} for resync"); - } - catch { } - } - } - catch (Exception e) - { - Main.helper.Log($"Error updating job system: {e.Message}"); - } - } - - Main.helper.Log("Villager state resync completed"); - } - catch (Exception e) - { - Main.helper.Log($"Error in delayed villager resync: {e.Message}"); - } - }); - } - catch (Exception e) - { - Main.helper.Log($"Error starting villager resync: {e.Message}"); - } - return obj; } } -} +} \ No newline at end of file From 4871f7c1506db1be7e22ae1bfe8184dddb29e641 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 23:14:50 +0100 Subject: [PATCH 05/17] =?UTF-8?q?4.=20pr=C3=B3ba...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index ea29ce2..8e3a963 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -70,7 +70,7 @@ namespace KCM.LoadSaveOverrides this.FireManagerSaveData = new FireManager.FireManagerSaveData().Pack(FireManager.inst); this.DragonSpawnSaveData = new DragonSpawn.DragonSpawnSaveData().Pack(DragonSpawn.inst); this.UnitSystemSaveData = new UnitSystem.UnitSystemSaveData().Pack(UnitSystem.inst); - this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(raiderSystem.inst); + this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(RaiderSystem.inst); if (ShipSystem.inst != null) { @@ -259,7 +259,7 @@ namespace KCM.LoadSaveOverrides bool flag8 = this.RaidSystemSaveData2 != null; if (flag8) { - this.RaidSystemSaveData2.Unpack(raiderSystem.inst); + this.RaidSystemSaveData2.Unpack(RaiderSystem.inst); } Main.helper.Log("Unpacking orders manager"); bool flag9 = this.OrdersManagerSaveData != null; @@ -268,7 +268,6 @@ namespace KCM.LoadSaveOverrides this.OrdersManagerSaveData.Unpack(OrdersManager.inst); } Main.helper.Log("Unpacking AI brains"); - bool flag10 = this.AIBrainsSaveData != null; if (flag10) { this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst); From 28e342b1e330033ed7e83cae371b2ed3c88b0931 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:23:06 +0100 Subject: [PATCH 06/17] pls :( --- Main.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Main.cs b/Main.cs index 7022717..f9ed6ea 100644 --- a/Main.cs +++ b/Main.cs @@ -1156,8 +1156,26 @@ namespace KCM Main.helper.Log($"set speed Called by 2: {new StackFrame(2).GetMethod()} {new StackFrame(2).GetMethod().Name.Contains("HandlePacket")}"); Main.helper.Log($"set speed Called by 3: {new StackFrame(3).GetMethod()} {new StackFrame(3).GetMethod().Name.Contains("HandlePacket")}");*/ - if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket")) - return; + try + { + if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket")) + return; + } + catch + { + } + + try + { + if (idx > 0 && Time.timeScale == 0f) + { + Time.timeScale = 1f; + Main.helper.Log("TimeScaleFix: restored Time.timeScale=1 on local SetSpeed idx=" + idx); + } + } + catch + { + } Main.helper.Log("SpeedControlUI.SetSpeed (local): " + idx); bool isPaused = (idx == 0); From e0b1b736c3a95653c725536ea38d44e34c7d42a1 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:27:17 +0100 Subject: [PATCH 07/17] pls :( 2 --- Main.cs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Main.cs b/Main.cs index f9ed6ea..e371bb1 100644 --- a/Main.cs +++ b/Main.cs @@ -58,6 +58,7 @@ namespace KCM private static readonly Dictionary lastTeamIdLookupLogMs = new Dictionary(); private static int resetInProgress = 0; private static int multiplayerSaveLoadInProgress = 0; + private static int worldReadyRebuildDone = 0; public static bool IsMultiplayerSaveLoadInProgress { @@ -94,6 +95,7 @@ namespace KCM try { LobbyManager.loadingSave = false; } catch { } try { SetMultiplayerSaveLoadInProgress(false); } catch { } + try { Interlocked.Exchange(ref worldReadyRebuildDone, 0); } catch { } try { @@ -160,6 +162,28 @@ namespace KCM } } + private static int GetLoadTickDelayOrMinusOne(object instance) + { + if (instance == null) + return -1; + + try + { + FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); + if (loadTickDelayField == null) + return -1; + + object v = loadTickDelayField.GetValue(instance); + if (v is int) + return (int)v; + } + catch + { + } + + return -1; + } + public static void RunPostLoadRebuild(string reason) { try @@ -318,6 +342,33 @@ namespace KCM private void FixedUpdate() { + try + { + if (KCClient.client != null && + KCClient.client.IsConnected && + Volatile.Read(ref worldReadyRebuildDone) == 0 && + World.inst != null && + Player.inst != null && + VillagerSystem.inst != null) + { + if (Interlocked.Exchange(ref worldReadyRebuildDone, 1) == 0) + { + Main.helper.Log("AutoRebuild: world ready; running post-load rebuild"); + RunPostLoadRebuild("auto:world-ready"); + Main.helper.Log( + "AutoRebuild: timeScale=" + Time.timeScale + + " loadTickDelay(Player/Unit/Job/Villager)=" + + GetLoadTickDelayOrMinusOne(Player.inst) + "/" + + GetLoadTickDelayOrMinusOne(UnitSystem.inst) + "/" + + GetLoadTickDelayOrMinusOne(JobSystem.inst) + "/" + + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); + } + } + } + catch + { + } + // send batched building placement info /*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0)) { From 3ee6fc4dc66d1329a113045b26c88081db8655f3 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:30:59 +0100 Subject: [PATCH 08/17] asd --- Main.cs | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/Main.cs b/Main.cs index e371bb1..fcf7bd7 100644 --- a/Main.cs +++ b/Main.cs @@ -155,7 +155,14 @@ namespace KCM { FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) + { loadTickDelayField.SetValue(instance, ticks); + return; + } + + PropertyInfo loadTickDelayProp = instance.GetType().GetProperty("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (loadTickDelayProp != null && loadTickDelayProp.CanWrite && loadTickDelayProp.PropertyType == typeof(int)) + loadTickDelayProp.SetValue(instance, ticks, null); } catch { @@ -170,12 +177,20 @@ namespace KCM try { FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); - if (loadTickDelayField == null) - return -1; + if (loadTickDelayField != null) + { + object v = loadTickDelayField.GetValue(instance); + if (v is int) + return (int)v; + } - object v = loadTickDelayField.GetValue(instance); - if (v is int) - return (int)v; + PropertyInfo loadTickDelayProp = instance.GetType().GetProperty("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (loadTickDelayProp != null && loadTickDelayProp.CanRead && loadTickDelayProp.PropertyType == typeof(int)) + { + object pv = loadTickDelayProp.GetValue(instance, null); + if (pv is int) + return (int)pv; + } } catch { @@ -339,6 +354,11 @@ namespace KCM #endregion public static int FixedUpdateInterval = 0; + private static long lastVillagerMoveMs = 0; + private static long lastVillagerProbeMs = 0; + private static long lastVillagerStallLogMs = 0; + private static Guid probedVillagerGuid = Guid.Empty; + private static Vector3 probedVillagerLastPos = Vector3.zero; private void FixedUpdate() { @@ -369,6 +389,74 @@ namespace KCM { } + try + { + if (KCClient.client != null && + KCClient.client.IsConnected && + World.inst != null && + Time.timeScale > 0f && + Villager.villagers != null && + Villager.villagers.Count > 0) + { + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastVillagerProbeMs) >= 2000) + { + lastVillagerProbeMs = now; + + Villager v = null; + try + { + if (probedVillagerGuid != Guid.Empty) + v = Villager.villagers.data.FirstOrDefault(x => x != null && x.guid == probedVillagerGuid); + } + catch + { + } + + if (v == null) + { + v = Villager.villagers.data.FirstOrDefault(x => x != null); + if (v != null) + { + probedVillagerGuid = v.guid; + probedVillagerLastPos = v.Pos; + lastVillagerMoveMs = now; + } + } + + if (v != null) + { + float movedSqr = (v.Pos - probedVillagerLastPos).sqrMagnitude; + if (movedSqr > 0.01f) + { + probedVillagerLastPos = v.Pos; + lastVillagerMoveMs = now; + } + + if ((now - lastVillagerMoveMs) >= 15000 && (now - lastVillagerStallLogMs) >= 15000) + { + lastVillagerStallLogMs = now; + Main.helper.Log( + "VillagerStallDetect: no movement for " + (now - lastVillagerMoveMs) + + "ms timeScale=" + Time.timeScale + + " villagers=" + Villager.villagers.Count + + " sampleGuid=" + probedVillagerGuid + + " samplePos=" + v.Pos); + Main.helper.Log( + "VillagerStallDetect: loadTickDelay(Player/Unit/Job/Villager)=" + + GetLoadTickDelayOrMinusOne(Player.inst) + "/" + + GetLoadTickDelayOrMinusOne(UnitSystem.inst) + "/" + + GetLoadTickDelayOrMinusOne(JobSystem.inst) + "/" + + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); + } + } + } + } + } + catch + { + } + // send batched building placement info /*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0)) { From 77f4d4fed0914a670a02ac5692b7b96c3bda3ce4 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:34:42 +0100 Subject: [PATCH 09/17] =?UTF-8?q?tal=C3=A1n=3F=3F=3F=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Main.cs | 39 ++++++++++++++++++++++++++++++++++++++ Packets/Lobby/StartGame.cs | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/Main.cs b/Main.cs index fcf7bd7..7f28611 100644 --- a/Main.cs +++ b/Main.cs @@ -199,6 +199,43 @@ namespace KCM return -1; } + private static string TryGetGameModeName() + { + try + { + if (GameState.inst == null) + return "null"; + + var t = GameState.inst.GetType(); + + var modeProp = t.GetProperty("mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? t.GetProperty("Mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? t.GetProperty("CurrentMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (modeProp != null) + { + object m = modeProp.GetValue(GameState.inst, null); + return m != null ? m.GetType().Name : "null"; + } + + var modeField = t.GetField("mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? t.GetField("Mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? t.GetField("currentMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? t.GetField("currMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (modeField != null) + { + object fm = modeField.GetValue(GameState.inst); + return fm != null ? fm.GetType().Name : "null"; + } + } + catch + { + } + + return "unknown"; + } + public static void RunPostLoadRebuild(string reason) { try @@ -439,6 +476,8 @@ namespace KCM Main.helper.Log( "VillagerStallDetect: no movement for " + (now - lastVillagerMoveMs) + "ms timeScale=" + Time.timeScale + + " mode=" + TryGetGameModeName() + + " villagerSystemEnabled=" + (VillagerSystem.inst != null && VillagerSystem.inst.enabled) + " villagers=" + Villager.villagers.Count + " sampleGuid=" + probedVillagerGuid + " samplePos=" + v.Pos); diff --git a/Packets/Lobby/StartGame.cs b/Packets/Lobby/StartGame.cs index eefcfcf..c861d49 100644 --- a/Packets/Lobby/StartGame.cs +++ b/Packets/Lobby/StartGame.cs @@ -39,6 +39,25 @@ namespace KCM.Packets.Lobby Main.helper.Log(ex.ToString()); } + try + { + GameState.inst.SetNewMode(GameState.inst.playingMode); + Main.helper.Log("StartGame: forced playing mode"); + } + catch (Exception ex) + { + Main.helper.Log("StartGame: failed forcing playing mode"); + Main.helper.Log(ex.ToString()); + } + + try + { + Main.RunPostLoadRebuild("StartGame"); + } + catch + { + } + SpeedControlUI.inst.SetSpeed(0); } else From 634a5f7983ec6ef58e1f7c0552e2dfb2784e66a5 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:42:03 +0100 Subject: [PATCH 10/17] sex --- Enums/Packets.cs | 3 +- .../GameVillager/VillagerSnapshotPacket.cs | 56 +++++++++++++++++++ Packets/Handlers/PacketHandler.cs | 44 +++++++++++++++ StateManagement/Sync/SyncManager.cs | 54 ++++++++++++++++++ 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 Packets/Game/GameVillager/VillagerSnapshotPacket.cs diff --git a/Enums/Packets.cs b/Enums/Packets.cs index 04eadfd..bc68b8c 100644 --- a/Enums/Packets.cs +++ b/Enums/Packets.cs @@ -47,6 +47,7 @@ namespace KCM.Enums PlaceKeepRandomly = 91, ResyncRequest = 92, ResourceSnapshot = 93, - BuildingSnapshot = 94 + BuildingSnapshot = 94, + VillagerSnapshot = 95 } } diff --git a/Packets/Game/GameVillager/VillagerSnapshotPacket.cs b/Packets/Game/GameVillager/VillagerSnapshotPacket.cs new file mode 100644 index 0000000..696890b --- /dev/null +++ b/Packets/Game/GameVillager/VillagerSnapshotPacket.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace KCM.Packets.Game.GameVillager +{ + public class VillagerSnapshotPacket : Packet + { + public override ushort packetId => (ushort)Enums.Packets.VillagerSnapshot; + + public List guids { get; set; } = new List(); + public List positions { get; set; } = new List(); + + public override void HandlePacketClient() + { + if (KCClient.client != null && clientId == KCClient.client.Id) + return; + + try + { + int count = Math.Min(guids?.Count ?? 0, positions?.Count ?? 0); + if (count == 0) + return; + + for (int i = 0; i < count; i++) + { + Guid guid = guids[i]; + Vector3 position = positions[i]; + + Villager villager = null; + try + { + villager = Villager.villagers?.data.FirstOrDefault(v => v != null && v.guid == guid); + } + catch + { + } + + if (villager == null) + continue; + + villager.TeleportTo(position); + } + } + catch (Exception e) + { + Main.helper.Log("Error handling villager snapshot packet: " + e.Message); + } + } + + public override void HandlePacketServer() + { + } + } +} diff --git a/Packets/Handlers/PacketHandler.cs b/Packets/Handlers/PacketHandler.cs index 90a91ef..3226503 100644 --- a/Packets/Handlers/PacketHandler.cs +++ b/Packets/Handlers/PacketHandler.cs @@ -310,6 +310,27 @@ namespace KCM.Packets.Handlers message.AddInt(item); } + else if (prop.PropertyType == typeof(List)) + { + currentPropName = prop.Name; + List list = (List)prop.GetValue(packet, null); + message.AddInt(list.Count); + foreach (var item in list) + message.AddBytes(item.ToByteArray(), true); + } + else if (prop.PropertyType == typeof(List)) + { + currentPropName = prop.Name; + List list = (List)prop.GetValue(packet, null); + message.AddInt(list.Count); + foreach (var item in list) + { + message.AddFloat(item.x); + message.AddFloat(item.y); + message.AddFloat(item.z); + } + } + else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { currentPropName = prop.Name; @@ -543,6 +564,29 @@ namespace KCM.Packets.Handlers prop.SetValue(p, list); } + else if (prop.PropertyType == typeof(List)) + { + int count = message.GetInt(); + List list = new List(); + + for (int i = 0; i < count; i++) + list.Add(new Guid(message.GetBytes())); + + prop.SetValue(p, list); + } + else if (prop.PropertyType == typeof(List)) + { + int count = message.GetInt(); + List list = new List(); + + for (int i = 0; i < count; i++) + { + Vector3 vector = new Vector3(message.GetFloat(), message.GetFloat(), message.GetFloat()); + list.Add(vector); + } + + prop.SetValue(p, list); + } else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { IDictionary dictionary = (IDictionary)prop.GetValue(p, null); diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 58404d5..9498098 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -16,9 +16,11 @@ namespace KCM.StateManagement.Sync private const int MaxBuildingSnapshotBytes = 30000; private const int MaxVillagerTeleportsPerResync = 400; private const int VillagerValidationIntervalMs = 10000; // 10 seconds + private const int VillagerSnapshotIntervalMs = 1000; private static long lastResourceBroadcastMs; private static long lastVillagerValidationMs; + private static long lastVillagerSnapshotMs; private static FieldInfo freeResourceAmountField; private static MethodInfo resourceAmountGetMethod; @@ -63,6 +65,20 @@ namespace KCM.StateManagement.Sync lastVillagerValidationMs = now; ValidateAndCorrectVillagerStates(); } + + if ((now - lastVillagerSnapshotMs) >= VillagerSnapshotIntervalMs) + { + lastVillagerSnapshotMs = now; + try + { + BroadcastVillagerSnapshot(); + } + catch (Exception ex) + { + Main.helper.Log("Error broadcasting villager snapshot"); + Main.helper.Log(ex.ToString()); + } + } } public static void SendResyncToClient(ushort toClient, string reason) @@ -559,6 +575,44 @@ namespace KCM.StateManagement.Sync } } + private static void BroadcastVillagerSnapshot() + { + if (!KCServer.IsRunning) + return; + + if (KCServer.server.ClientCount == 0) + return; + + if (Villager.villagers == null || Villager.villagers.Count == 0) + return; + + List guids = new List(); + List positions = new List(); + const int maxVillagersPerSnapshot = 200; + + for (int i = 0; i < Villager.villagers.Count && guids.Count < maxVillagersPerSnapshot; i++) + { + Villager villager = Villager.villagers.data[i]; + if (villager == null) + continue; + + guids.Add(villager.guid); + positions.Add(villager.Pos); + } + + if (guids.Count == 0) + return; + + VillagerSnapshotPacket snapshot = new VillagerSnapshotPacket + { + guids = guids, + positions = positions + }; + + ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0; + snapshot.SendToAll(exceptId); + } + public static void ApplyResourceSnapshot(List resourceTypes, List amounts) { if (resourceTypes == null || amounts == null) From d6c0ec2a330dec87254cfe908ae93c167fce1008 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:50:36 +0100 Subject: [PATCH 11/17] sae --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index cad1a6d..4c7b39c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ Desktop.ini **/obj/ **/*.mdb **/*.pdb + +/.claude \ No newline at end of file From 4a2c73badb6d61711429baf660835f9f17bb5894 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:50:45 +0100 Subject: [PATCH 12/17] asd --- AGENTS.md | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index cbb6dff..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization - -- `Main.cs`: primary Harmony patches, gameplay hooks, and high-level multiplayer flow. -- `Packets/`: network message types and handlers (client/server). Subfolders group by domain (e.g., `Lobby`, `Game`, `State`, `Handlers`). -- `LoadSaveOverrides/`: multiplayer-aware save/load containers and BinaryFormatter binder. -- `StateManagement/`: observer-based state syncing (e.g., building state updates). -- `ServerBrowser/`, `ServerLobby/`, `UI/`: menu screens, lobby UI, and related scripts/prefabs glue. -- `Riptide/`, `RiptideSteamTransport/`: networking and Steam transport integration. -- `Enums/`, `Constants.cs`, `ErrorCodeMessages.cs`, `ReflectionHelper/`: shared types/utilities. - -## Build, Test, and Development Commands - -This mod is typically compiled/loaded by the game’s mod loader (there is no `.csproj` here). - -- Validate changes quickly: `rg -n "TODO|FIXME|throw|NotImplementedException" -S .` -- Inspect recent log output: `Get-Content .\\output.txt -Tail 200` -- Check history/context: `git log -n 20 --oneline` - -To run locally, copy/enable the mod in *Kingdoms and Castles* and **fully restart the game** after changes. Keep host/client mod versions identical. - -## Coding Style & Naming Conventions - -- Language: C# (Unity/Mono). Prefer conservative language features to avoid in-game compiler issues. -- Indentation: 4 spaces; braces on new lines (match existing files). -- Names: `PascalCase` for types/methods, `camelCase` for locals/fields. Packet properties are public and serialized—treat renames as breaking changes. -- Logging: use `Main.helper.Log(...)` with short, searchable messages. - -## Testing Guidelines - -No automated test suite. Verify in-game with a minimal repro: - -- Host ↔ join, place buildings, save/load, leave/rejoin, and confirm sync. -- When reporting bugs, include `output.txt` excerpts around the first exception and “Save Transfer” markers. - -## Commit & Pull Request Guidelines - -Git history uses short, informal summaries. For contributions: - -- Commits: one-line, descriptive, avoid profanity; include a scope when helpful (e.g., `save: fix load fallback`). -- PRs: describe the issue, repro steps, expected vs actual, and attach relevant `output.txt` snippets. Note game version, mod version, and whether it’s Workshop or local mod folder. - -## Agent-Specific Notes - -- Avoid edits that depend on newer C# syntax not supported by the runtime compiler. -- Prefer small, isolated fixes; multiplayer regressions are easy to introduce—add logs around save/load and connect/disconnect paths. From 2140fc386896103b9f7080f7ff1ab6125613a440 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:54:55 +0100 Subject: [PATCH 13/17] =?UTF-8?q?nem=20tudom=20most=20claude=20tal=C3=A1n?= =?UTF-8?q?=20megoldja=20xd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Main.cs | 26 +++++++++++++++++++++++--- StateManagement/Sync/SyncManager.cs | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Main.cs b/Main.cs index 7f28611..50195f1 100644 --- a/Main.cs +++ b/Main.cs @@ -153,7 +153,7 @@ namespace KCM try { - FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); + FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (loadTickDelayField != null) { loadTickDelayField.SetValue(instance, ticks); @@ -164,8 +164,9 @@ namespace KCM if (loadTickDelayProp != null && loadTickDelayProp.CanWrite && loadTickDelayProp.PropertyType == typeof(int)) loadTickDelayProp.SetValue(instance, ticks, null); } - catch + catch (Exception e) { + helper?.Log("SetLoadTickDelay failed for " + instance.GetType().Name + ": " + e.Message); } } @@ -176,7 +177,7 @@ namespace KCM try { - FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); + FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (loadTickDelayField != null) { object v = loadTickDelayField.GetValue(instance); @@ -247,10 +248,17 @@ namespace KCM try { Player.inst.irrigation.UpdateIrrigation(); } catch (Exception e) { helper?.Log(e.ToString()); } try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { helper?.Log(e.ToString()); } + helper?.Log("Setting loadTickDelay for game systems"); SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1); SetLoadTickDelay(VillagerSystem.inst, 1); + + helper?.Log( + "loadTickDelay after set: Player=" + GetLoadTickDelayOrMinusOne(Player.inst) + + " Unit=" + GetLoadTickDelayOrMinusOne(UnitSystem.inst) + + " Job=" + GetLoadTickDelayOrMinusOne(JobSystem.inst) + + " Villager=" + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); } catch (Exception e) { @@ -487,6 +495,18 @@ namespace KCM GetLoadTickDelayOrMinusOne(UnitSystem.inst) + "/" + GetLoadTickDelayOrMinusOne(JobSystem.inst) + "/" + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); + + // Try to fix stalled systems by resetting loadTickDelay + if (GetLoadTickDelayOrMinusOne(VillagerSystem.inst) == -1 || + GetLoadTickDelayOrMinusOne(UnitSystem.inst) == -1 || + GetLoadTickDelayOrMinusOne(JobSystem.inst) == -1) + { + Main.helper.Log("VillagerStallDetect: Attempting to fix stalled systems"); + SetLoadTickDelay(Player.inst, 1); + SetLoadTickDelay(UnitSystem.inst, 1); + SetLoadTickDelay(JobSystem.inst, 1); + SetLoadTickDelay(VillagerSystem.inst, 1); + } } } } diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 9498098..394eb7c 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -588,7 +588,7 @@ namespace KCM.StateManagement.Sync List guids = new List(); List positions = new List(); - const int maxVillagersPerSnapshot = 200; + const int maxVillagersPerSnapshot = 50; for (int i = 0; i < Villager.villagers.Count && guids.Count < maxVillagersPerSnapshot; i++) { From 42a86419caddb00d55d8560d92589e587fe3f95f Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 00:59:48 +0100 Subject: [PATCH 14/17] jaj aja nem hinnem xd --- Main.cs | 31 +++++++++++++++++++++++++++++ StateManagement/Sync/SyncManager.cs | 16 +++++++++------ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Main.cs b/Main.cs index 50195f1..e1b5fa4 100644 --- a/Main.cs +++ b/Main.cs @@ -162,7 +162,27 @@ namespace KCM PropertyInfo loadTickDelayProp = instance.GetType().GetProperty("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (loadTickDelayProp != null && loadTickDelayProp.CanWrite && loadTickDelayProp.PropertyType == typeof(int)) + { loadTickDelayProp.SetValue(instance, ticks, null); + return; + } + + // Debug: list all fields if loadTickDelay not found + if (instance.GetType().Name == "VillagerSystem") + { + helper?.Log("DEBUG: VillagerSystem fields:"); + foreach (var field in instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + if (field.FieldType == typeof(int) || field.FieldType == typeof(bool)) + helper?.Log($" Field: {field.Name} ({field.FieldType.Name})"); + } + helper?.Log("DEBUG: VillagerSystem properties:"); + foreach (var prop in instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(bool)) + helper?.Log($" Property: {prop.Name} ({prop.PropertyType.Name})"); + } + } } catch (Exception e) { @@ -259,6 +279,17 @@ namespace KCM " Unit=" + GetLoadTickDelayOrMinusOne(UnitSystem.inst) + " Job=" + GetLoadTickDelayOrMinusOne(JobSystem.inst) + " Villager=" + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); + + // Try to enable VillagerSystem if it's disabled + if (VillagerSystem.inst != null && !VillagerSystem.inst.enabled) + { + helper?.Log("VillagerSystem is disabled, enabling it"); + VillagerSystem.inst.enabled = true; + } + else if (VillagerSystem.inst != null) + { + helper?.Log("VillagerSystem.enabled = " + VillagerSystem.inst.enabled); + } } catch (Exception e) { diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 394eb7c..792d0fc 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -577,14 +577,18 @@ namespace KCM.StateManagement.Sync private static void BroadcastVillagerSnapshot() { - if (!KCServer.IsRunning) - return; + // TEMPORARILY DISABLED: VillagerSnapshot causes packet overflow errors + // TODO: Fix villager synchronization properly + return; - if (KCServer.server.ClientCount == 0) - return; + // if (!KCServer.IsRunning) + // return; - if (Villager.villagers == null || Villager.villagers.Count == 0) - return; + // if (KCServer.server.ClientCount == 0) + // return; + + // if (Villager.villagers == null || Villager.villagers.Count == 0) + // return; List guids = new List(); List positions = new List(); From 414ab90afca589d940530ec6f8e70c32975acb48 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 01:05:37 +0100 Subject: [PATCH 15/17] =?UTF-8?q?aha=20persze=20tal=C3=A1n=20most=20nem=20?= =?UTF-8?q?szia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 13 ++++++++++++ Main.cs | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 8e3a963..8de3ee8 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -332,6 +332,19 @@ namespace KCM.LoadSaveOverrides Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}"); TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]); + // Force transition to PlayingMode to enable villager movement + try + { + Main.helper.Log("MultiplayerSaveContainer.Unpack: Setting PlayingMode"); + GameState.inst.SetNewMode(GameState.inst.playingMode); + Main.helper.Log("MultiplayerSaveContainer.Unpack: PlayingMode set successfully"); + } + catch (Exception e) + { + Main.helper.Log("MultiplayerSaveContainer.Unpack: Failed to set PlayingMode"); + Main.helper.Log(e.ToString()); + } + return obj; } } diff --git a/Main.cs b/Main.cs index e1b5fa4..2a681fb 100644 --- a/Main.cs +++ b/Main.cs @@ -290,6 +290,26 @@ namespace KCM { helper?.Log("VillagerSystem.enabled = " + VillagerSystem.inst.enabled); } + + // Force transition to PlayingMode to enable villager movement + try + { + if (GameState.inst != null) + { + helper?.Log("RunPostLoadRebuild: Current GameState mode: " + TryGetGameModeName()); + if (GameState.inst.playingMode != null) + { + helper?.Log("RunPostLoadRebuild: Setting PlayingMode"); + GameState.inst.SetNewMode(GameState.inst.playingMode); + helper?.Log("RunPostLoadRebuild: PlayingMode set successfully"); + } + } + } + catch (Exception ex) + { + helper?.Log("RunPostLoadRebuild: Failed to set PlayingMode"); + helper?.Log(ex.ToString()); + } } catch (Exception e) { From 93c55dd48201435517ea9bbde3b42112c8a5f8c4 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 01:10:03 +0100 Subject: [PATCH 16/17] bruh --- LoadSaveOverrides/MultiplayerSaveContainer.cs | 13 ------------ Main.cs | 20 ------------------- StateManagement/Sync/SyncManager.cs | 16 ++++++--------- 3 files changed, 6 insertions(+), 43 deletions(-) diff --git a/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 8de3ee8..8e3a963 100644 --- a/LoadSaveOverrides/MultiplayerSaveContainer.cs +++ b/LoadSaveOverrides/MultiplayerSaveContainer.cs @@ -332,19 +332,6 @@ namespace KCM.LoadSaveOverrides Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}"); TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]); - // Force transition to PlayingMode to enable villager movement - try - { - Main.helper.Log("MultiplayerSaveContainer.Unpack: Setting PlayingMode"); - GameState.inst.SetNewMode(GameState.inst.playingMode); - Main.helper.Log("MultiplayerSaveContainer.Unpack: PlayingMode set successfully"); - } - catch (Exception e) - { - Main.helper.Log("MultiplayerSaveContainer.Unpack: Failed to set PlayingMode"); - Main.helper.Log(e.ToString()); - } - return obj; } } diff --git a/Main.cs b/Main.cs index 2a681fb..e1b5fa4 100644 --- a/Main.cs +++ b/Main.cs @@ -290,26 +290,6 @@ namespace KCM { helper?.Log("VillagerSystem.enabled = " + VillagerSystem.inst.enabled); } - - // Force transition to PlayingMode to enable villager movement - try - { - if (GameState.inst != null) - { - helper?.Log("RunPostLoadRebuild: Current GameState mode: " + TryGetGameModeName()); - if (GameState.inst.playingMode != null) - { - helper?.Log("RunPostLoadRebuild: Setting PlayingMode"); - GameState.inst.SetNewMode(GameState.inst.playingMode); - helper?.Log("RunPostLoadRebuild: PlayingMode set successfully"); - } - } - } - catch (Exception ex) - { - helper?.Log("RunPostLoadRebuild: Failed to set PlayingMode"); - helper?.Log(ex.ToString()); - } } catch (Exception e) { diff --git a/StateManagement/Sync/SyncManager.cs b/StateManagement/Sync/SyncManager.cs index 792d0fc..394eb7c 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -577,18 +577,14 @@ namespace KCM.StateManagement.Sync private static void BroadcastVillagerSnapshot() { - // TEMPORARILY DISABLED: VillagerSnapshot causes packet overflow errors - // TODO: Fix villager synchronization properly - return; + if (!KCServer.IsRunning) + return; - // if (!KCServer.IsRunning) - // return; + if (KCServer.server.ClientCount == 0) + return; - // if (KCServer.server.ClientCount == 0) - // return; - - // if (Villager.villagers == null || Villager.villagers.Count == 0) - // return; + if (Villager.villagers == null || Villager.villagers.Count == 0) + return; List guids = new List(); List positions = new List(); From 914650c211aa7ef7e9099e3029ab0fc6a2295ec2 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sun, 14 Dec 2025 01:17:20 +0100 Subject: [PATCH 17/17] asd --- Main.cs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Main.cs b/Main.cs index e1b5fa4..19930d3 100644 --- a/Main.cs +++ b/Main.cs @@ -479,6 +479,22 @@ namespace KCM { lastVillagerProbeMs = now; + // Proactively check and fix loadTickDelay every 2 seconds + int villagerDelay = GetLoadTickDelayOrMinusOne(VillagerSystem.inst); + int unitDelay = GetLoadTickDelayOrMinusOne(UnitSystem.inst); + int jobDelay = GetLoadTickDelayOrMinusOne(JobSystem.inst); + int playerDelay = GetLoadTickDelayOrMinusOne(Player.inst); + + if (villagerDelay <= 0 || unitDelay <= 0 || jobDelay <= 0 || playerDelay <= 0) + { + Main.helper.Log("LoadTickDelay refresh: delays were " + + playerDelay + "/" + unitDelay + "/" + jobDelay + "/" + villagerDelay + ", resetting to 1"); + SetLoadTickDelay(Player.inst, 1); + SetLoadTickDelay(UnitSystem.inst, 1); + SetLoadTickDelay(JobSystem.inst, 1); + SetLoadTickDelay(VillagerSystem.inst, 1); + } + Villager v = null; try { @@ -528,11 +544,15 @@ namespace KCM GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); // Try to fix stalled systems by resetting loadTickDelay - if (GetLoadTickDelayOrMinusOne(VillagerSystem.inst) == -1 || - GetLoadTickDelayOrMinusOne(UnitSystem.inst) == -1 || - GetLoadTickDelayOrMinusOne(JobSystem.inst) == -1) + int villagerDelay = GetLoadTickDelayOrMinusOne(VillagerSystem.inst); + int unitDelay = GetLoadTickDelayOrMinusOne(UnitSystem.inst); + int jobDelay = GetLoadTickDelayOrMinusOne(JobSystem.inst); + int playerDelay = GetLoadTickDelayOrMinusOne(Player.inst); + + if (villagerDelay <= 0 || unitDelay <= 0 || jobDelay <= 0 || playerDelay <= 0) { - Main.helper.Log("VillagerStallDetect: Attempting to fix stalled systems"); + Main.helper.Log("VillagerStallDetect: Attempting to fix stalled systems (delays: " + + playerDelay + "/" + unitDelay + "/" + jobDelay + "/" + villagerDelay + ")"); SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1);