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 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. 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/LoadSaveOverrides/MultiplayerSaveContainer.cs b/LoadSaveOverrides/MultiplayerSaveContainer.cs index 0a5e6de..8e3a963 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; @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using UnityEngine; namespace KCM.LoadSaveOverrides { @@ -24,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 @@ -102,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)) @@ -120,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); @@ -132,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; @@ -149,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)) @@ -165,39 +157,63 @@ 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"); - /* - * Not even going to bother fixing AI brains save data yet, not in short-term roadmap - */ - - /*bool flag2 = this.AIBrainsSaveData != null; - if (flag2) + Main.helper.Log("Unpacking AI brains"); + bool flag10 = this.AIBrainsSaveData != null; + if (flag10) { - this.AIBrainsSaveData.UnpackPrePlayer(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(); + 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); + } + } Main.helper.Log("Unpacking free resource manager"); this.FreeResourceManagerSaveData.Unpack(FreeResourceManager.inst); @@ -252,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); @@ -279,7 +294,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++) @@ -287,36 +301,32 @@ namespace KCM.LoadSaveOverrides Player.inst.Buildings.data[i].UpdateMaterialSelection(); } - // Player.inst.loadTickDelay = 1; 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; 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; 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; 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); } Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}"); @@ -325,4 +335,4 @@ namespace KCM.LoadSaveOverrides return obj; } } -} +} \ No newline at end of file diff --git a/Main.cs b/Main.cs index 1e772a7..19930d3 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; @@ -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 { @@ -151,13 +153,108 @@ 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); + 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); + 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) + { + helper?.Log("SetLoadTickDelay failed for " + instance.GetType().Name + ": " + e.Message); + } + } + + private static int GetLoadTickDelayOrMinusOne(object instance) + { + if (instance == null) + return -1; + + try + { + FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (loadTickDelayField != null) + { + 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 { } + + 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) @@ -171,10 +268,28 @@ 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)); + + // 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) { @@ -315,9 +430,143 @@ 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() { + 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 + { + } + + 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; + + // 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 + { + 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 + + " mode=" + TryGetGameModeName() + + " villagerSystemEnabled=" + (VillagerSystem.inst != null && VillagerSystem.inst.enabled) + + " 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)); + + // Try to fix stalled systems by resetting loadTickDelay + 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 (delays: " + + playerDelay + "/" + unitDelay + "/" + jobDelay + "/" + villagerDelay + ")"); + SetLoadTickDelay(Player.inst, 1); + SetLoadTickDelay(UnitSystem.inst, 1); + SetLoadTickDelay(JobSystem.inst, 1); + SetLoadTickDelay(VillagerSystem.inst, 1); + } + } + } + } + } + } + catch + { + } + // send batched building placement info /*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0)) { @@ -1156,13 +1405,33 @@ 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); new SetSpeed() { - speed = idx + speed = idx, + isPaused = isPaused }.Send(); lastTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); 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/Game/SetSpeed.cs b/Packets/Game/SetSpeed.cs index 319eb71..be6b8b1 100644 --- a/Packets/Game/SetSpeed.cs +++ b/Packets/Game/SetSpeed.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using UnityEngine; namespace KCM.Packets.Game { @@ -11,18 +12,76 @@ 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 + { + // Force villager system refresh to ensure they continue working + if (VillagerSystem.inst != 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) + { + 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 refreshed for speed change to {speed}"); + } + catch (Exception e) + { + Main.helper.Log("Error refreshing 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/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/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 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 84d8f60..394eb7c 100644 --- a/StateManagement/Sync/SyncManager.cs +++ b/StateManagement/Sync/SyncManager.cs @@ -15,8 +15,12 @@ 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 const int VillagerSnapshotIntervalMs = 1000; private static long lastResourceBroadcastMs; + private static long lastVillagerValidationMs; + private static long lastVillagerSnapshotMs; private static FieldInfo freeResourceAmountField; private static MethodInfo resourceAmountGetMethod; @@ -30,27 +34,50 @@ 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(); + } + + if ((now - lastVillagerSnapshotMs) >= VillagerSnapshotIntervalMs) + { + lastVillagerSnapshotMs = now; + try + { + BroadcastVillagerSnapshot(); + } + catch (Exception ex) + { + Main.helper.Log("Error broadcasting villager snapshot"); + Main.helper.Log(ex.ToString()); + } } } @@ -548,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 = 50; + + 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) @@ -731,5 +796,95 @@ 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 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 + { + // Ensure valid position + if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z)) + { + // Teleport to a safe position + Vector3 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}"); + } + + // 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) + { + Main.helper.Log("Error in villager state validation: " + e.Message); + } + } } }