Merge branch 'talan-fix/2'

This commit is contained in:
2025-12-14 01:18:52 +01:00
11 changed files with 676 additions and 107 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ Desktop.ini
**/obj/
**/*.mdb
**/*.pdb
/.claude

View File

@@ -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 games 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 its 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.

View File

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

View File

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

279
Main.cs
View File

@@ -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<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>();
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();

View File

@@ -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<Guid> guids { get; set; } = new List<Guid>();
public List<Vector3> positions { get; set; } = new List<Vector3>();
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()
{
}
}
}

View File

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

View File

@@ -310,6 +310,27 @@ namespace KCM.Packets.Handlers
message.AddInt(item);
}
else if (prop.PropertyType == typeof(List<Guid>))
{
currentPropName = prop.Name;
List<Guid> list = (List<Guid>)prop.GetValue(packet, null);
message.AddInt(list.Count);
foreach (var item in list)
message.AddBytes(item.ToByteArray(), true);
}
else if (prop.PropertyType == typeof(List<Vector3>))
{
currentPropName = prop.Name;
List<Vector3> list = (List<Vector3>)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<Guid>))
{
int count = message.GetInt();
List<Guid> list = new List<Guid>();
for (int i = 0; i < count; i++)
list.Add(new Guid(message.GetBytes()));
prop.SetValue(p, list);
}
else if (prop.PropertyType == typeof(List<Vector3>))
{
int count = message.GetInt();
List<Vector3> list = new List<Vector3>();
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);

View File

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

View File

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

View File

@@ -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<Guid> guids = new List<Guid>();
List<Vector3> positions = new List<Vector3>();
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<int> resourceTypes, List<int> 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);
}
}
}
}