From 537787575b269ef090ff47f968d93dd3ac2af7cf Mon Sep 17 00:00:00 2001 From: devbeni Date: Sat, 13 Dec 2025 22:33:36 +0100 Subject: [PATCH] sex --- Enums/Packets.cs | 3 +- Main.cs | 279 ++++++++++++++++++ Packets/Network/SpeedRestoreRequestPacket.cs | 39 +++ ServerLobby/ServerLobbyScript.cs | 30 ++ .../BuildingState/BuildingStateManager.cs | 1 + 5 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 Packets/Network/SpeedRestoreRequestPacket.cs diff --git a/Enums/Packets.cs b/Enums/Packets.cs index 04eadfd..220da3c 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, + SpeedRestoreRequest = 95 } } diff --git a/Main.cs b/Main.cs index b6848e3..d31ff19 100644 --- a/Main.cs +++ b/Main.cs @@ -59,6 +59,11 @@ namespace KCM private static int resetInProgress = 0; private static int multiplayerSaveLoadInProgress = 0; private static int suppressVillagerTeleportPackets = 0; + private static int lastAppliedSpeedIndex = 0; + private static long lastSimWatchdogFixMs = 0; + private static int lastNonZeroSpeedIndex = 1; + private static int forceAllowClientLocalSpeedSetOnce = 0; + private static long lastSimStateLogMs = 0; public static bool IsMultiplayerSaveLoadInProgress { @@ -80,6 +85,143 @@ namespace KCM Interlocked.Exchange(ref suppressVillagerTeleportPackets, suppress ? 1 : 0); } + public static int LastAppliedSpeedIndex + { + get { return Volatile.Read(ref lastAppliedSpeedIndex); } + } + + public static int LastNonZeroSpeedIndex + { + get { return Volatile.Read(ref lastNonZeroSpeedIndex); } + } + + private static void SetLastAppliedSpeedIndex(int idx) + { + Interlocked.Exchange(ref lastAppliedSpeedIndex, idx); + if (idx > 0) + Interlocked.Exchange(ref lastNonZeroSpeedIndex, idx); + } + + private static void ForceAllowClientLocalSpeedOnce() + { + Interlocked.Exchange(ref forceAllowClientLocalSpeedSetOnce, 1); + } + + private static bool ConsumeForceAllowClientLocalSpeedOnce() + { + return Interlocked.Exchange(ref forceAllowClientLocalSpeedSetOnce, 0) == 1; + } + + private static int TryGetLoadTickDelay(object instance) + { + if (instance == null) + return -1; + + try + { + FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); + if (loadTickDelayField != null && loadTickDelayField.FieldType == typeof(int)) + return (int)loadTickDelayField.GetValue(instance); + } + catch + { + } + + return -1; + } + + private static void LogSimState(string reason) + { + try + { + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastSimStateLogMs) < 1000) + return; + + lastSimStateLogMs = now; + + bool connected = false; + try { connected = KCClient.client != null && KCClient.client.IsConnected; } catch { } + + bool focused = true; + try { focused = Application.isFocused; } catch { } + + helper?.Log($"SimState ({(reason ?? string.Empty)}): menu={(int)menuState} server={KCServer.IsRunning} connected={connected} focused={focused} timeScale={Time.timeScale:0.###} speedLast={LastAppliedSpeedIndex} speedNonZero={LastNonZeroSpeedIndex}"); + helper?.Log($"SimState systems: Player={(Player.inst != null)} UnitSys={(UnitSystem.inst != null)} JobSys={(JobSystem.inst != null)} VillagerSys={(VillagerSystem.inst != null)}"); + + try { helper?.Log($"SimState enabled: UnitSys={(UnitSystem.inst != null && UnitSystem.inst.enabled)} JobSys={(JobSystem.inst != null && JobSystem.inst.enabled)} VillagerSys={(VillagerSystem.inst != null && VillagerSystem.inst.enabled)}"); } catch { } + try { helper?.Log($"SimState loadTickDelay: Player={TryGetLoadTickDelay(Player.inst)} UnitSys={TryGetLoadTickDelay(UnitSystem.inst)} JobSys={TryGetLoadTickDelay(JobSystem.inst)} VillagerSys={TryGetLoadTickDelay(VillagerSystem.inst)}"); } catch { } + + try + { + int playerWorkers = Player.inst != null ? Player.inst.Workers.Count : -1; + int allVillagers = Villager.villagers != null ? Villager.villagers.Count : -1; + helper?.Log($"SimState villagers: playerWorkers={playerWorkers} allVillagers={allVillagers}"); + } + catch + { + } + } + catch + { + } + } + + private static void TryForceSetSpeed(int speedIdx, string reason) + { + if (speedIdx <= 0) + return; + + try + { + if (SpeedControlUI.inst == null) + return; + + if (!KCServer.IsRunning) + ForceAllowClientLocalSpeedOnce(); + + helper?.Log($"ForceSetSpeed: {speedIdx} ({reason ?? string.Empty})"); + SpeedControlUI.inst.SetSpeed(speedIdx); + } + catch (Exception ex) + { + helper?.Log("ForceSetSpeed failed"); + helper?.Log(ex.ToString()); + } + } + + public static void DumpSimState(string reason) + { + LogSimState(reason); + } + + public static void Unstuck(string reason) + { + try + { + bool connected = KCClient.client != null && KCClient.client.IsConnected; + if (!connected && !KCServer.IsRunning) + return; + + LogSimState("unstuck:" + (reason ?? string.Empty)); + + if (!KCServer.IsRunning) + { + try { new KCM.Packets.Network.SpeedRestoreRequestPacket { reason = reason ?? string.Empty }.Send(); } catch { } + } + else + { + int speed = LastNonZeroSpeedIndex > 0 ? LastNonZeroSpeedIndex : 1; + TryForceSetSpeed(speed, "unstuck:" + (reason ?? string.Empty)); + } + + try { RunPostLoadRebuild("unstuck:" + (reason ?? string.Empty)); } catch { } + } + catch + { + } + } + public static void ResetMultiplayerState(string reason = null) { if (Interlocked.Exchange(ref resetInProgress, 1) == 1) @@ -446,6 +588,123 @@ namespace KCM public static int FixedUpdateInterval = 0; + private void Update() + { + try + { + bool connected = KCClient.client != null && KCClient.client.IsConnected; + if (!connected && !KCServer.IsRunning) + return; + + try + { + if (Input.GetKeyDown(KeyCode.F8)) + LogSimState("hotkey:F8"); + + if (Input.GetKeyDown(KeyCode.F9) && connected) + { + if (!KCServer.IsRunning) + { + try { new KCM.Packets.Network.SpeedRestoreRequestPacket { reason = "hotkey:F9" }.Send(); } catch { } + } + else + { + TryForceSetSpeed(LastNonZeroSpeedIndex > 0 ? LastNonZeroSpeedIndex : 1, "hotkey:F9"); + } + + try { RunPostLoadRebuild("unstuck:hotkey:F9"); } catch { } + } + } + catch + { + } + + // Only apply watchdog behavior in playing mode. + if ((int)menuState != 200) + return; + + // If something pauses the game by setting Time.timeScale without going through SpeedControlUI, + // villagers + events will appear "stuck". We reapply the last known speed. + if (LastAppliedSpeedIndex > 0 && Time.timeScale == 0f) + { + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if ((now - lastSimWatchdogFixMs) >= 2000) + { + lastSimWatchdogFixMs = now; + + LogSimState("watchdog:timescale0"); + TryForceSetSpeed(LastAppliedSpeedIndex, "watchdog:timescale0"); + } + } + + // Best-effort recovery if core simulation systems are disabled after load. + try { if (UnitSystem.inst != null && !UnitSystem.inst.enabled) UnitSystem.inst.enabled = true; } catch { } + try { if (JobSystem.inst != null && !JobSystem.inst.enabled) JobSystem.inst.enabled = true; } catch { } + try { if (VillagerSystem.inst != null && !VillagerSystem.inst.enabled) VillagerSystem.inst.enabled = true; } catch { } + } + catch + { + } + } + + private void OnApplicationFocus(bool hasFocus) + { + try + { + if (!hasFocus) + return; + + bool connected = KCClient.client != null && KCClient.client.IsConnected; + if (!connected && !KCServer.IsRunning) + return; + + if ((int)menuState != 200) + return; + + if (Time.timeScale != 0f) + return; + + int speed = LastAppliedSpeedIndex > 0 ? LastAppliedSpeedIndex : LastNonZeroSpeedIndex; + if (speed <= 0) + speed = 1; + + LogSimState("focus-gained"); + TryForceSetSpeed(speed, "focus-gained"); + } + catch + { + } + } + + private void OnApplicationPause(bool pauseStatus) + { + try + { + if (pauseStatus) + return; + + bool connected = KCClient.client != null && KCClient.client.IsConnected; + if (!connected && !KCServer.IsRunning) + return; + + if ((int)menuState != 200) + return; + + if (Time.timeScale != 0f) + return; + + int speed = LastAppliedSpeedIndex > 0 ? LastAppliedSpeedIndex : LastNonZeroSpeedIndex; + if (speed <= 0) + speed = 1; + + LogSimState("unpaused"); + TryForceSetSpeed(speed, "unpaused"); + } + catch + { + } + } + private void FixedUpdate() { // send batched building placement info @@ -948,6 +1207,15 @@ namespace KCM { if (KCClient.client.IsConnected) { + // Only override AddBuilding for remote "Client Player" instances. + // The vanilla implementation does important registration work for the local player (jobs, systems, etc). + // When loading/unpacking for a remote player we temporarily set Player.inst to that player, so vanilla is safe. + if (__instance == null || __instance == Player.inst) + return true; + + if (__instance.gameObject == null || __instance.gameObject.name == null || !__instance.gameObject.name.Contains("Client Player")) + return true; + __instance.Buildings.Add(b); IResourceStorage[] storages = b.GetComponents(); for (int i = 0; i < storages.Length; i++) @@ -1262,7 +1530,16 @@ namespace KCM if (!KCServer.IsRunning) { if (calledFromPacket) + { + SetLastAppliedSpeedIndex(idx); return true; + } + + if (idx > 0 && ConsumeForceAllowClientLocalSpeedOnce()) + { + SetLastAppliedSpeedIndex(idx); + return true; + } long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); if ((now - lastClientBlockLogTime) >= 2000) @@ -1274,6 +1551,8 @@ namespace KCM return false; } + SetLastAppliedSpeedIndex(idx); + if (!calledFromPacket) { long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); diff --git a/Packets/Network/SpeedRestoreRequestPacket.cs b/Packets/Network/SpeedRestoreRequestPacket.cs new file mode 100644 index 0000000..d5c40b1 --- /dev/null +++ b/Packets/Network/SpeedRestoreRequestPacket.cs @@ -0,0 +1,39 @@ +using KCM.Attributes; +using KCM.Packets.Game; +using System; + +namespace KCM.Packets.Network +{ + [NoServerRelay] + public class SpeedRestoreRequestPacket : Packet + { + public override ushort packetId => (ushort)Enums.Packets.SpeedRestoreRequest; + + public string reason { get; set; } + + public override void HandlePacketClient() + { + } + + public override void HandlePacketServer() + { + try + { + if (!KCServer.IsRunning) + return; + + int speed = Main.LastNonZeroSpeedIndex; + if (speed <= 0) + speed = 1; + + Main.helper.Log($"Speed restore requested by client {clientId} ({reason ?? string.Empty}); broadcasting speed {speed}"); + new SetSpeed { speed = speed }.SendToAll(); + } + catch (Exception ex) + { + Main.helper.Log("Error handling SpeedRestoreRequestPacket on server"); + Main.helper.Log(ex.ToString()); + } + } + } +} diff --git a/ServerLobby/ServerLobbyScript.cs b/ServerLobby/ServerLobbyScript.cs index c1bfbb5..1535b04 100644 --- a/ServerLobby/ServerLobbyScript.cs +++ b/ServerLobby/ServerLobbyScript.cs @@ -219,6 +219,36 @@ namespace KCM { if (ChatInput.text.Length > 0) { + if (ChatInput.text.Trim().Equals("/simdebug", StringComparison.OrdinalIgnoreCase)) + { + try + { + Main.DumpSimState("chat:/simdebug"); + LobbyHandler.AddSystemMessage("Sim state logged to output.txt"); + } + catch + { + } + + ChatInput.text = ""; + return; + } + + if (ChatInput.text.Trim().Equals("/unstuck", StringComparison.OrdinalIgnoreCase)) + { + try + { + Main.Unstuck("chat:/unstuck"); + LobbyHandler.AddSystemMessage("Unstuck requested."); + } + catch + { + } + + ChatInput.text = ""; + return; + } + if (ChatInput.text.Trim().Equals("/resync", StringComparison.OrdinalIgnoreCase)) { try diff --git a/StateManagement/BuildingState/BuildingStateManager.cs b/StateManagement/BuildingState/BuildingStateManager.cs index d6b9f23..ebc00f4 100644 --- a/StateManagement/BuildingState/BuildingStateManager.cs +++ b/StateManagement/BuildingState/BuildingStateManager.cs @@ -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