Merge branch 'talan-fix/2'
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ Desktop.ini
|
||||
**/obj/
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
47
AGENTS.md
47
AGENTS.md
@@ -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.
|
||||
@@ -47,6 +47,7 @@ namespace KCM.Enums
|
||||
PlaceKeepRandomly = 91,
|
||||
ResyncRequest = 92,
|
||||
ResourceSnapshot = 93,
|
||||
BuildingSnapshot = 94
|
||||
BuildingSnapshot = 94,
|
||||
VillagerSnapshot = 95
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]}");
|
||||
|
||||
275
Main.cs
275
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<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")}");*/
|
||||
|
||||
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();
|
||||
|
||||
56
Packets/Game/GameVillager/VillagerSnapshotPacket.cs
Normal file
56
Packets/Game/GameVillager/VillagerSnapshotPacket.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +34,10 @@ namespace KCM.StateManagement.Sync
|
||||
return;
|
||||
|
||||
long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
if ((now - lastResourceBroadcastMs) < ResourceBroadcastIntervalMs)
|
||||
return;
|
||||
|
||||
// Resource broadcast
|
||||
if ((now - lastResourceBroadcastMs) >= ResourceBroadcastIntervalMs)
|
||||
{
|
||||
lastResourceBroadcastMs = now;
|
||||
|
||||
try
|
||||
@@ -54,6 +59,28 @@ namespace KCM.StateManagement.Sync
|
||||
}
|
||||
}
|
||||
|
||||
// Villager state validation
|
||||
if ((now - lastVillagerValidationMs) >= VillagerValidationIntervalMs)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendResyncToClient(ushort toClient, string reason)
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user