This commit is contained in:
2025-12-13 22:33:36 +01:00
parent dc50bf2892
commit 537787575b
5 changed files with 351 additions and 1 deletions

View File

@@ -47,6 +47,7 @@ namespace KCM.Enums
PlaceKeepRandomly = 91,
ResyncRequest = 92,
ResourceSnapshot = 93,
BuildingSnapshot = 94
BuildingSnapshot = 94,
SpeedRestoreRequest = 95
}
}

279
Main.cs
View File

@@ -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<IResourceStorage>();
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();

View File

@@ -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());
}
}
}
}

View File

@@ -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

View File

@@ -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