From a2d87106bae9db14661377e95621cd3c3d316e9f Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 20:21:25 +0100 Subject: [PATCH 1/4] =?UTF-8?q?tal=C3=A1n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Main.cs | 95 +++++++++++++++++++++++++------ Packets/Game/SetSpeed.cs | 2 +- Packets/Handlers/PacketHandler.cs | 13 +++++ Packets/Lobby/StartGame.cs | 4 +- 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/Main.cs b/Main.cs index 1e772a7..e387c8a 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 suppressVillagerTeleportPackets = 0; public static bool IsMultiplayerSaveLoadInProgress { @@ -69,6 +70,16 @@ namespace KCM Interlocked.Exchange(ref multiplayerSaveLoadInProgress, inProgress ? 1 : 0); } + private static bool ShouldSuppressVillagerTeleportPackets + { + get { return Volatile.Read(ref suppressVillagerTeleportPackets) != 0; } + } + + private static void SetSuppressVillagerTeleportPackets(bool suppress) + { + Interlocked.Exchange(ref suppressVillagerTeleportPackets, suppress ? 1 : 0); + } + public static void ResetMultiplayerState(string reason = null) { if (Interlocked.Exchange(ref resetInProgress, 1) == 1) @@ -171,10 +182,51 @@ 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()); } + try { if (UnitSystem.inst != null) UnitSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); } + try { if (JobSystem.inst != null) JobSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); } + try { if (VillagerSystem.inst != null) VillagerSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); } + SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1); SetLoadTickDelay(VillagerSystem.inst, 1); + + try + { + // A nudge helps recover from cases where villagers have jobs but never begin moving. + SetSuppressVillagerTeleportPackets(true); + foreach (var kcPlayer in kCPlayers.Values) + { + if (kcPlayer == null || kcPlayer.inst == null) + continue; + + var workers = kcPlayer.inst.Workers; + for (int i = 0; i < workers.Count; i++) + { + Villager v = workers.data[i]; + if (v == null) + continue; + + try + { + Vector3 pos = v.transform != null ? v.transform.position : Vector3.zero; + v.TeleportTo(pos); + } + catch + { + } + } + } + } + catch (Exception e) + { + helper?.Log("Post-load villager nudge failed"); + helper?.Log(e.ToString()); + } + finally + { + SetSuppressVillagerTeleportPackets(false); + } } catch (Exception e) { @@ -434,6 +486,26 @@ namespace KCM if ((MenuState)newState == MenuState.Menu && (KCClient.client.IsConnected || KCServer.IsRunning)) ResetMultiplayerState("Returned to main menu"); + + if ((MenuState)newState == (MenuState)200 && KCClient.client.IsConnected) + { + try + { + RunPostLoadRebuild("Entered playing mode"); + } + catch + { + } + + try + { + if (SpeedControlUI.inst != null) + SpeedControlUI.inst.SetSpeed(1); + } + catch + { + } + } } } @@ -1126,13 +1198,7 @@ namespace KCM if (KCClient.client.IsConnected) { bool calledFromPacket = false; - try - { - calledFromPacket = new StackFrame(3).GetMethod().Name.Contains("HandlePacket"); - } - catch - { - } + try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { } if (!calledFromPacket) { @@ -1151,14 +1217,6 @@ namespace KCM if (!__state) return; - /*Main.helper.Log($"set speed Called by 0: {new StackFrame(0).GetMethod()} {new StackFrame(0).GetMethod().Name.Contains("HandlePacket")}"); - Main.helper.Log($"set speed Called by 1: {new StackFrame(1).GetMethod()} {new StackFrame(1).GetMethod().Name.Contains("HandlePacket")}"); - 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; - Main.helper.Log("SpeedControlUI.SetSpeed (local): " + idx); new SetSpeed() { @@ -1274,7 +1332,12 @@ namespace KCM { if (KCClient.client.IsConnected) { - if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket")) + if (ShouldSuppressVillagerTeleportPackets) + return; + + bool calledFromPacket = false; + try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { } + if (calledFromPacket) return; new VillagerTeleportTo() diff --git a/Packets/Game/SetSpeed.cs b/Packets/Game/SetSpeed.cs index 319eb71..4824d1d 100644 --- a/Packets/Game/SetSpeed.cs +++ b/Packets/Game/SetSpeed.cs @@ -22,7 +22,7 @@ namespace KCM.Packets.Game public override void HandlePacketServer() { - //throw new NotImplementedException(); + // Server relay is handled automatically by PacketHandler unless [NoServerRelay] is used. } } } diff --git a/Packets/Handlers/PacketHandler.cs b/Packets/Handlers/PacketHandler.cs index 90a91ef..f2adc25 100644 --- a/Packets/Handlers/PacketHandler.cs +++ b/Packets/Handlers/PacketHandler.cs @@ -15,6 +15,14 @@ namespace KCM.Packets.Handlers { public class PacketHandler { + [ThreadStatic] + private static bool isHandlingPacket; + + public static bool IsHandlingPacket + { + get { return isHandlingPacket; } + } + public static Dictionary Packets = new Dictionary(); public class PacketRef { @@ -183,6 +191,7 @@ namespace KCM.Packets.Handlers { try { + isHandlingPacket = true; packet.HandlePacketClient(); } catch (Exception ex) @@ -205,6 +214,10 @@ namespace KCM.Packets.Handlers Main.helper.Log(ex.InnerException.StackTrace); } } + finally + { + isHandlingPacket = false; + } } /* if (PacketHandlers.TryGetValue(id, out PacketHandlerDelegate handler)) diff --git a/Packets/Lobby/StartGame.cs b/Packets/Lobby/StartGame.cs index eefcfcf..ab2ab39 100644 --- a/Packets/Lobby/StartGame.cs +++ b/Packets/Lobby/StartGame.cs @@ -25,6 +25,7 @@ namespace KCM.Packets.Lobby try { + int desiredSpeed = 1; if (!LobbyManager.loadingSave) { SpeedControlUI.inst.SetSpeed(0); @@ -39,12 +40,13 @@ namespace KCM.Packets.Lobby Main.helper.Log(ex.ToString()); } - SpeedControlUI.inst.SetSpeed(0); + SpeedControlUI.inst.SetSpeed(desiredSpeed); } else { LobbyManager.loadingSave = false; GameState.inst.SetNewMode(GameState.inst.playingMode); + SpeedControlUI.inst.SetSpeed(desiredSpeed); } } catch (Exception ex) From fbb947a23b28c5b7c28ea4996398fd57c7829d33 Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 20:24:54 +0100 Subject: [PATCH 2/4] fix --- Main.cs | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/Main.cs b/Main.cs index e387c8a..3f2040e 100644 --- a/Main.cs +++ b/Main.cs @@ -171,6 +171,83 @@ namespace KCM } } + private static bool TryGetVillagerPosition(Villager villager, out Vector3 position) + { + position = Vector3.zero; + if (villager == null) + return false; + + try + { + var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + Type type = villager.GetType(); + + string[] gameObjectNames = new string[] { "gameObject", "go", "Go" }; + for (int i = 0; i < gameObjectNames.Length; i++) + { + string name = gameObjectNames[i]; + + PropertyInfo prop = type.GetProperty(name, flags); + if (prop != null && typeof(GameObject).IsAssignableFrom(prop.PropertyType)) + { + GameObject go = prop.GetValue(villager, null) as GameObject; + if (go != null) + { + position = go.transform.position; + return true; + } + } + + FieldInfo field = type.GetField(name, flags); + if (field != null && typeof(GameObject).IsAssignableFrom(field.FieldType)) + { + GameObject go = field.GetValue(villager) as GameObject; + if (go != null) + { + position = go.transform.position; + return true; + } + } + } + + string[] positionNames = new string[] { "pos", "Pos", "position", "Position" }; + for (int i = 0; i < positionNames.Length; i++) + { + string name = positionNames[i]; + + PropertyInfo prop = type.GetProperty(name, flags); + if (prop != null && prop.PropertyType == typeof(Vector3)) + { + position = (Vector3)prop.GetValue(villager, null); + return true; + } + + FieldInfo field = type.GetField(name, flags); + if (field != null && field.FieldType == typeof(Vector3)) + { + position = (Vector3)field.GetValue(villager); + return true; + } + } + + string[] getPosNames = new string[] { "GetPos", "GetPosition" }; + for (int i = 0; i < getPosNames.Length; i++) + { + MethodInfo method = type.GetMethod(getPosNames[i], flags, null, new Type[0], null); + if (method != null && method.ReturnType == typeof(Vector3)) + { + position = (Vector3)method.Invoke(villager, null); + return true; + } + } + } + catch + { + } + + return false; + } + public static void RunPostLoadRebuild(string reason) { try @@ -204,18 +281,19 @@ namespace KCM for (int i = 0; i < workers.Count; i++) { Villager v = workers.data[i]; - if (v == null) - continue; + if (v == null) + continue; - try - { - Vector3 pos = v.transform != null ? v.transform.position : Vector3.zero; + try + { + Vector3 pos; + if (TryGetVillagerPosition(v, out pos)) v.TeleportTo(pos); - } - catch - { - } } + catch + { + } + } } } catch (Exception e) From e636ad6e19ccb4cc3bb3bafeedfe43a146ad58cb Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 20:57:23 +0100 Subject: [PATCH 3/4] asd --- Main.cs | 51 ++++++++++++++++++++--------- Packets/Lobby/SaveTransferPacket.cs | 8 +++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Main.cs b/Main.cs index 3f2040e..03d6b24 100644 --- a/Main.cs +++ b/Main.cs @@ -1269,20 +1269,38 @@ namespace KCM public class SpeedControlUISetSpeedHook { private static long lastTime = 0; + private static long lastClientBlockLogTime = 0; - public static bool Prefix(ref bool __state) + public static bool Prefix(int idx, ref bool __state) { __state = false; - if (KCClient.client.IsConnected) - { - bool calledFromPacket = false; - try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { } + if (!KCClient.client.IsConnected) + return true; - if (!calledFromPacket) + bool calledFromPacket = false; + try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { } + + // In multiplayer, keep time control authoritative to the host to avoid clients pausing/stalling the simulation. + if (!KCServer.IsRunning) + { + if (calledFromPacket) + return true; + + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastClientBlockLogTime) >= 2000) { - if ((DateTimeOffset.Now.ToUnixTimeMilliseconds() - lastTime) >= 250) // Set speed spam fix / hack - __state = true; + lastClientBlockLogTime = now; + Main.helper.Log("Blocked SpeedControlUI.SetSpeed on non-host client: " + idx); } + + return false; + } + + if (!calledFromPacket) + { + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastTime) >= 250) // Set speed spam fix / hack + __state = true; } return true; @@ -1463,18 +1481,13 @@ namespace KCM public static bool Prefix(ref string __result) { Main.helper.Log("Get save dir"); - if (KCClient.client.IsConnected) + if (KCServer.IsRunning) { - if (KCServer.IsRunning) - { - - } __result = Application.persistentDataPath + "/Saves/Multiplayer"; - return false; } - __result = Application.persistentDataPath + "/Saves"; ; + __result = Application.persistentDataPath + "/Saves"; return true; } } @@ -1554,6 +1567,14 @@ namespace KCM Main.SetMultiplayerSaveLoadInProgress(false); } + try + { + RunPostLoadRebuild("LoadAtPath (multiplayer)"); + } + catch + { + } + Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); } diff --git a/Packets/Lobby/SaveTransferPacket.cs b/Packets/Lobby/SaveTransferPacket.cs index d3e1374..0bd1eb9 100644 --- a/Packets/Lobby/SaveTransferPacket.cs +++ b/Packets/Lobby/SaveTransferPacket.cs @@ -117,6 +117,14 @@ namespace KCM.Packets.Lobby { Main.SetMultiplayerSaveLoadInProgress(false); } + + try + { + RunPostLoadRebuild("Save transfer complete"); + } + catch + { + } Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); try From dc50bf2892185bdbf192a00937a77d4acec2eeeb Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 21:46:58 +0100 Subject: [PATCH 4/4] fix??? --- KCClient.cs | 1 + Main.cs | 44 +++++++------- .../BuildingState/BuildingStateManager.cs | 57 +++++++++++++++++-- StateManagement/Observers/Observer.cs | 25 ++++++++ 4 files changed, 96 insertions(+), 31 deletions(-) diff --git a/KCClient.cs b/KCClient.cs index 9e530c6..d81a7d4 100644 --- a/KCClient.cs +++ b/KCClient.cs @@ -102,6 +102,7 @@ namespace KCM public static void Connect(string ip) { Main.helper.Log("Trying to connect to: " + ip); + try { Application.runInBackground = true; } catch { } client.Connect(ip, useMessageHandlers: false); } diff --git a/Main.cs b/Main.cs index 03d6b24..b6848e3 100644 --- a/Main.cs +++ b/Main.cs @@ -942,26 +942,14 @@ namespace KCM [HarmonyPatch(typeof(Player), "AddBuilding")] public class PlayerAddBuildingHook { - static int step = 1; - static void LogStep(bool reset = false) - { - if (reset) - step = 1; - - Main.helper.Log(step.ToString()); - step++; - } - public static bool Prefix(Player __instance, Building b) { try { if (KCClient.client.IsConnected) { - LogStep(true); __instance.Buildings.Add(b); IResourceStorage[] storages = b.GetComponents(); - LogStep(); for (int i = 0; i < storages.Length; i++) { bool flag = !storages[i].IsPrivate(); @@ -970,50 +958,38 @@ namespace KCM FreeResourceManager.inst.AddResourceStorage(storages[i]); } } - LogStep(); int landMass = b.LandMass(); Home res = b.GetComponent(); bool flag2 = res != null; - LogStep(); if (flag2) { __instance.Residentials.Add(res); __instance.ResidentialsPerLandmass[landMass].Add(res); } WagePayer wagePayer = b.GetComponent(); - LogStep(); bool flag3 = wagePayer != null; if (flag3) { __instance.WagePayers.Add(wagePayer); } RadiusBonus radiusBonus = b.GetComponent(); - LogStep(); bool flag4 = radiusBonus != null; if (flag4) { __instance.RadiusBonuses.Add(radiusBonus); } - LogStep(); var globalBuildingRegistry = __instance.GetType().GetField("globalBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt; - LogStep(); var landMassBuildingRegistry = __instance.GetType().GetField("landMassBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt; - LogStep(); var unbuiltBuildingsPerLandmass = __instance.GetType().GetField("unbuiltBuildingsPerLandmass", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt>; - LogStep(); __instance.AddToRegistry(globalBuildingRegistry, b); - LogStep(); __instance.AddToRegistry(landMassBuildingRegistry.data[landMass].registry, b); - LogStep(); landMassBuildingRegistry.data[landMass].buildings.Add(b); - LogStep(); bool flag5 = !b.IsBuilt(); if (flag5) { unbuiltBuildingsPerLandmass.data[landMass].Add(b); } - LogStep(); return false; @@ -1270,6 +1246,8 @@ namespace KCM { private static long lastTime = 0; private static long lastClientBlockLogTime = 0; + private static long lastHostPauseTraceLogTime = 0; + private static int lastSentSpeed = -1; public static bool Prefix(int idx, ref bool __state) { @@ -1299,8 +1277,23 @@ namespace KCM if (!calledFromPacket) { long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - if ((now - lastTime) >= 250) // Set speed spam fix / hack + // Ensure that real speed changes are always propagated, even if they happen in quick succession (eg. pause/unpause). + if (idx != lastSentSpeed || (now - lastTime) >= 250) // Set speed spam fix / hack __state = true; + + // Diagnostics for "random pause": log a stack trace when the host hits speed 0 from local code. + if (idx == 0 && (now - lastHostPauseTraceLogTime) >= 2000) + { + lastHostPauseTraceLogTime = now; + try + { + Main.helper.Log("Host speed set to 0 (pause). Call stack:"); + Main.helper.Log(new StackTrace(2, false).ToString()); + } + catch + { + } + } } return true; @@ -1320,6 +1313,7 @@ namespace KCM }.Send(); lastTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + lastSentSpeed = idx; } } } diff --git a/StateManagement/BuildingState/BuildingStateManager.cs b/StateManagement/BuildingState/BuildingStateManager.cs index 7be888a..d6b9f23 100644 --- a/StateManagement/BuildingState/BuildingStateManager.cs +++ b/StateManagement/BuildingState/BuildingStateManager.cs @@ -22,27 +22,72 @@ namespace KCM.StateManagement.BuildingState { try { - Observer observer = (Observer)sender; + Observer observer = sender as Observer; + if (observer == null) + return; - Building building = (Building)observer.state; + Building building = observer.state as Building; + if (building == null) + return; //Main.helper.Log("Should send building network update for: " + building.UniqueName); + var t = building.transform; + if (t == null) + return; + + Quaternion rotation = t.rotation; + Vector3 globalPosition = t.position; + Vector3 localPosition = t.localPosition; + + if (t.childCount > 0) + { + try + { + var child = t.GetChild(0); + if (child != null) + { + rotation = child.rotation; + localPosition = child.localPosition; + } + } + catch + { + } + } + + float resourceProgress = 0f; + try + { + var field = building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (field != null) + { + object value = field.GetValue(building); + if (value is float) + resourceProgress = (float)value; + else if (value != null) + resourceProgress = Convert.ToSingle(value); + } + } + catch + { + } + new BuildingStatePacket() { customName = building.customName, guid = building.guid, uniqueName = building.UniqueName, - rotation = building.transform.GetChild(0).rotation, - globalPosition = building.transform.position, - localPosition = building.transform.GetChild(0).localPosition, + rotation = rotation, + globalPosition = globalPosition, + localPosition = localPosition, built = building.IsBuilt(), placed = building.IsPlaced(), open = building.Open, doBuildAnimation = building.doBuildAnimation, constructionPaused = building.constructionPaused, constructionProgress = building.constructionProgress, - resourceProgress = (float)building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(building), + resourceProgress = resourceProgress, life = building.Life, ModifiedMaxLife = building.ModifiedMaxLife, yearBuilt = building.YearBuilt, diff --git a/StateManagement/Observers/Observer.cs b/StateManagement/Observers/Observer.cs index 7f7fc77..ef22fbf 100644 --- a/StateManagement/Observers/Observer.cs +++ b/StateManagement/Observers/Observer.cs @@ -128,6 +128,31 @@ namespace KCM.StateManagement.Observers if (this.state == null) return; + // Unity uses "fake null" for destroyed objects. Since our state is stored as object, + // we must explicitly detect that case to avoid exceptions + log spam. + try + { + UnityEngine.Object unityObj = this.state as UnityEngine.Object; + if (this.state is UnityEngine.Object && unityObj == null) + { + try { StateObserver.observers.Remove(this.state.GetHashCode()); } catch { } + try + { + if (observerObject != null) + UnityEngine.Object.Destroy(observerObject); + else + UnityEngine.Object.Destroy(this.gameObject); + } + catch + { + } + return; + } + } + catch + { + } + if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds); return;