This commit is contained in:
2025-12-14 01:19:31 +01:00
8 changed files with 289 additions and 134 deletions

View File

@@ -102,6 +102,7 @@ namespace KCM
public static void Connect(string ip) public static void Connect(string ip)
{ {
Main.helper.Log("Trying to connect to: " + ip); Main.helper.Log("Trying to connect to: " + ip);
try { Application.runInBackground = true; } catch { }
client.Connect(ip, useMessageHandlers: false); client.Connect(ip, useMessageHandlers: false);
} }

294
Main.cs
View File

@@ -1,4 +1,4 @@
using Assets.Code; using Assets.Code;
using Assets.Code.UI; using Assets.Code.UI;
using Assets.Interface; using Assets.Interface;
using Harmony; using Harmony;
@@ -58,7 +58,7 @@ namespace KCM
private static readonly Dictionary<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>(); private static readonly Dictionary<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>();
private static int resetInProgress = 0; private static int resetInProgress = 0;
private static int multiplayerSaveLoadInProgress = 0; private static int multiplayerSaveLoadInProgress = 0;
private static int worldReadyRebuildDone = 0; private static int suppressVillagerTeleportPackets = 0;
public static bool IsMultiplayerSaveLoadInProgress public static bool IsMultiplayerSaveLoadInProgress
{ {
@@ -70,6 +70,16 @@ namespace KCM
Interlocked.Exchange(ref multiplayerSaveLoadInProgress, inProgress ? 1 : 0); Interlocked.Exchange(ref multiplayerSaveLoadInProgress, inProgress ? 1 : 0);
} }
private static bool ShouldSuppressVillagerTeleportPackets
{
get { return Volatile.Read(ref suppressVillagerTeleportPackets) != 0; }
}
private static void SetSuppressVillagerTeleportPackets(bool suppress)
{
Interlocked.Exchange(ref suppressVillagerTeleportPackets, suppress ? 1 : 0);
}
public static void ResetMultiplayerState(string reason = null) public static void ResetMultiplayerState(string reason = null)
{ {
if (Interlocked.Exchange(ref resetInProgress, 1) == 1) if (Interlocked.Exchange(ref resetInProgress, 1) == 1)
@@ -216,45 +226,83 @@ namespace KCM
catch catch
{ {
} }
return -1;
} }
private static string TryGetGameModeName() private static bool TryGetVillagerPosition(Villager villager, out Vector3 position)
{ {
position = Vector3.zero;
if (villager == null)
return false;
try try
{ {
if (GameState.inst == null) var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
return "null"; Type type = villager.GetType();
var t = GameState.inst.GetType(); string[] gameObjectNames = new string[] { "gameObject", "go", "Go" };
for (int i = 0; i < gameObjectNames.Length; i++)
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); string name = gameObjectNames[i];
return m != null ? m.GetType().Name : "null";
PropertyInfo prop = type.GetProperty(name, flags);
if (prop != null && typeof(GameObject).IsAssignableFrom(prop.PropertyType))
{
GameObject go = prop.GetValue(villager, null) as GameObject;
if (go != null)
{
position = go.transform.position;
return true;
}
}
FieldInfo field = type.GetField(name, flags);
if (field != null && typeof(GameObject).IsAssignableFrom(field.FieldType))
{
GameObject go = field.GetValue(villager) as GameObject;
if (go != null)
{
position = go.transform.position;
return true;
}
}
} }
var modeField = t.GetField("mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) string[] positionNames = new string[] { "pos", "Pos", "position", "Position" };
?? t.GetField("Mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) for (int i = 0; i < positionNames.Length; i++)
?? 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); string name = positionNames[i];
return fm != null ? fm.GetType().Name : "null";
PropertyInfo prop = type.GetProperty(name, flags);
if (prop != null && prop.PropertyType == typeof(Vector3))
{
position = (Vector3)prop.GetValue(villager, null);
return true;
}
FieldInfo field = type.GetField(name, flags);
if (field != null && field.FieldType == typeof(Vector3))
{
position = (Vector3)field.GetValue(villager);
return true;
}
}
string[] getPosNames = new string[] { "GetPos", "GetPosition" };
for (int i = 0; i < getPosNames.Length; i++)
{
MethodInfo method = type.GetMethod(getPosNames[i], flags, null, new Type[0], null);
if (method != null && method.ReturnType == typeof(Vector3))
{
position = (Vector3)method.Invoke(villager, null);
return true;
}
} }
} }
catch catch
{ {
} }
return "unknown"; return false;
} }
public static void RunPostLoadRebuild(string reason) public static void RunPostLoadRebuild(string reason)
@@ -268,27 +316,51 @@ namespace KCM
try { Player.inst.irrigation.UpdateIrrigation(); } catch (Exception e) { helper?.Log(e.ToString()); } 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()); } try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { helper?.Log(e.ToString()); }
helper?.Log("Setting loadTickDelay for game systems"); try { if (UnitSystem.inst != null) UnitSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); }
try { if (JobSystem.inst != null) JobSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); }
try { if (VillagerSystem.inst != null) VillagerSystem.inst.enabled = true; } catch (Exception e) { helper?.Log(e.ToString()); }
SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(Player.inst, 1);
SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1);
SetLoadTickDelay(JobSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1);
SetLoadTickDelay(VillagerSystem.inst, 1); SetLoadTickDelay(VillagerSystem.inst, 1);
helper?.Log( try
"loadTickDelay after set: Player=" + GetLoadTickDelayOrMinusOne(Player.inst) + {
" Unit=" + GetLoadTickDelayOrMinusOne(UnitSystem.inst) + // A nudge helps recover from cases where villagers have jobs but never begin moving.
" Job=" + GetLoadTickDelayOrMinusOne(JobSystem.inst) + SetSuppressVillagerTeleportPackets(true);
" Villager=" + GetLoadTickDelayOrMinusOne(VillagerSystem.inst)); foreach (var kcPlayer in kCPlayers.Values)
{
if (kcPlayer == null || kcPlayer.inst == null)
continue;
// Try to enable VillagerSystem if it's disabled var workers = kcPlayer.inst.Workers;
if (VillagerSystem.inst != null && !VillagerSystem.inst.enabled) for (int i = 0; i < workers.Count; i++)
{ {
helper?.Log("VillagerSystem is disabled, enabling it"); Villager v = workers.data[i];
VillagerSystem.inst.enabled = true; if (v == null)
continue;
try
{
Vector3 pos;
if (TryGetVillagerPosition(v, out pos))
v.TeleportTo(pos);
}
catch
{
}
}
}
} }
else if (VillagerSystem.inst != null) catch (Exception e)
{ {
helper?.Log("VillagerSystem.enabled = " + VillagerSystem.inst.enabled); helper?.Log("Post-load villager nudge failed");
helper?.Log(e.ToString());
}
finally
{
SetSuppressVillagerTeleportPackets(false);
} }
} }
catch (Exception e) catch (Exception e)
@@ -683,6 +755,26 @@ namespace KCM
if ((MenuState)newState == MenuState.Menu && (KCClient.client.IsConnected || KCServer.IsRunning)) if ((MenuState)newState == MenuState.Menu && (KCClient.client.IsConnected || KCServer.IsRunning))
ResetMultiplayerState("Returned to main menu"); ResetMultiplayerState("Returned to main menu");
if ((MenuState)newState == (MenuState)200 && KCClient.client.IsConnected)
{
try
{
RunPostLoadRebuild("Entered playing mode");
}
catch
{
}
try
{
if (SpeedControlUI.inst != null)
SpeedControlUI.inst.SetSpeed(1);
}
catch
{
}
}
} }
} }
@@ -1041,26 +1133,14 @@ namespace KCM
[HarmonyPatch(typeof(Player), "AddBuilding")] [HarmonyPatch(typeof(Player), "AddBuilding")]
public class PlayerAddBuildingHook public class PlayerAddBuildingHook
{ {
static int step = 1;
static void LogStep(bool reset = false)
{
if (reset)
step = 1;
Main.helper.Log(step.ToString());
step++;
}
public static bool Prefix(Player __instance, Building b) public static bool Prefix(Player __instance, Building b)
{ {
try try
{ {
if (KCClient.client.IsConnected) if (KCClient.client.IsConnected)
{ {
LogStep(true);
__instance.Buildings.Add(b); __instance.Buildings.Add(b);
IResourceStorage[] storages = b.GetComponents<IResourceStorage>(); IResourceStorage[] storages = b.GetComponents<IResourceStorage>();
LogStep();
for (int i = 0; i < storages.Length; i++) for (int i = 0; i < storages.Length; i++)
{ {
bool flag = !storages[i].IsPrivate(); bool flag = !storages[i].IsPrivate();
@@ -1069,50 +1149,38 @@ namespace KCM
FreeResourceManager.inst.AddResourceStorage(storages[i]); FreeResourceManager.inst.AddResourceStorage(storages[i]);
} }
} }
LogStep();
int landMass = b.LandMass(); int landMass = b.LandMass();
Home res = b.GetComponent<Home>(); Home res = b.GetComponent<Home>();
bool flag2 = res != null; bool flag2 = res != null;
LogStep();
if (flag2) if (flag2)
{ {
__instance.Residentials.Add(res); __instance.Residentials.Add(res);
__instance.ResidentialsPerLandmass[landMass].Add(res); __instance.ResidentialsPerLandmass[landMass].Add(res);
} }
WagePayer wagePayer = b.GetComponent<WagePayer>(); WagePayer wagePayer = b.GetComponent<WagePayer>();
LogStep();
bool flag3 = wagePayer != null; bool flag3 = wagePayer != null;
if (flag3) if (flag3)
{ {
__instance.WagePayers.Add(wagePayer); __instance.WagePayers.Add(wagePayer);
} }
RadiusBonus radiusBonus = b.GetComponent<RadiusBonus>(); RadiusBonus radiusBonus = b.GetComponent<RadiusBonus>();
LogStep();
bool flag4 = radiusBonus != null; bool flag4 = radiusBonus != null;
if (flag4) if (flag4)
{ {
__instance.RadiusBonuses.Add(radiusBonus); __instance.RadiusBonuses.Add(radiusBonus);
} }
LogStep();
var globalBuildingRegistry = __instance.GetType().GetField("globalBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<Player.BuildingRegistry>; var globalBuildingRegistry = __instance.GetType().GetField("globalBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<Player.BuildingRegistry>;
LogStep();
var landMassBuildingRegistry = __instance.GetType().GetField("landMassBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<Player.LandMassBuildingRegistry>; var landMassBuildingRegistry = __instance.GetType().GetField("landMassBuildingRegistry", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<Player.LandMassBuildingRegistry>;
LogStep();
var unbuiltBuildingsPerLandmass = __instance.GetType().GetField("unbuiltBuildingsPerLandmass", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<ArrayExt<Building>>; var unbuiltBuildingsPerLandmass = __instance.GetType().GetField("unbuiltBuildingsPerLandmass", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(__instance) as ArrayExt<ArrayExt<Building>>;
LogStep();
__instance.AddToRegistry(globalBuildingRegistry, b); __instance.AddToRegistry(globalBuildingRegistry, b);
LogStep();
__instance.AddToRegistry(landMassBuildingRegistry.data[landMass].registry, b); __instance.AddToRegistry(landMassBuildingRegistry.data[landMass].registry, b);
LogStep();
landMassBuildingRegistry.data[landMass].buildings.Add(b); landMassBuildingRegistry.data[landMass].buildings.Add(b);
LogStep();
bool flag5 = !b.IsBuilt(); bool flag5 = !b.IsBuilt();
if (flag5) if (flag5)
{ {
unbuiltBuildingsPerLandmass.data[landMass].Add(b); unbuiltBuildingsPerLandmass.data[landMass].Add(b);
} }
LogStep();
return false; return false;
@@ -1368,25 +1436,54 @@ namespace KCM
public class SpeedControlUISetSpeedHook public class SpeedControlUISetSpeedHook
{ {
private static long lastTime = 0; private static long lastTime = 0;
private static long lastClientBlockLogTime = 0;
private static long lastHostPauseTraceLogTime = 0;
private static int lastSentSpeed = -1;
public static bool Prefix(ref bool __state) public static bool Prefix(int idx, ref bool __state)
{ {
__state = false; __state = false;
if (KCClient.client.IsConnected) if (!KCClient.client.IsConnected)
return true;
bool calledFromPacket = false;
try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { }
// In multiplayer, keep time control authoritative to the host to avoid clients pausing/stalling the simulation.
if (!KCServer.IsRunning)
{ {
bool calledFromPacket = false; if (calledFromPacket)
try return true;
{
calledFromPacket = new StackFrame(3).GetMethod().Name.Contains("HandlePacket"); long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
} if ((now - lastClientBlockLogTime) >= 2000)
catch
{ {
lastClientBlockLogTime = now;
Main.helper.Log("Blocked SpeedControlUI.SetSpeed on non-host client: " + idx);
} }
if (!calledFromPacket) return false;
}
if (!calledFromPacket)
{
long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
// Ensure that real speed changes are always propagated, even if they happen in quick succession (eg. pause/unpause).
if (idx != lastSentSpeed || (now - lastTime) >= 250) // Set speed spam fix / hack
__state = true;
// Diagnostics for "random pause": log a stack trace when the host hits speed 0 from local code.
if (idx == 0 && (now - lastHostPauseTraceLogTime) >= 2000)
{ {
if ((DateTimeOffset.Now.ToUnixTimeMilliseconds() - lastTime) >= 250) // Set speed spam fix / hack lastHostPauseTraceLogTime = now;
__state = true; try
{
Main.helper.Log("Host speed set to 0 (pause). Call stack:");
Main.helper.Log(new StackTrace(2, false).ToString());
}
catch
{
}
} }
} }
@@ -1400,32 +1497,6 @@ namespace KCM
if (!__state) if (!__state)
return; return;
/*Main.helper.Log($"set speed Called by 0: {new StackFrame(0).GetMethod()} {new StackFrame(0).GetMethod().Name.Contains("HandlePacket")}");
Main.helper.Log($"set speed Called by 1: {new StackFrame(1).GetMethod()} {new StackFrame(1).GetMethod().Name.Contains("HandlePacket")}");
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); Main.helper.Log("SpeedControlUI.SetSpeed (local): " + idx);
bool isPaused = (idx == 0); bool isPaused = (idx == 0);
new SetSpeed() new SetSpeed()
@@ -1435,6 +1506,7 @@ namespace KCM
}.Send(); }.Send();
lastTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); lastTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
lastSentSpeed = idx;
} }
} }
} }
@@ -1543,7 +1615,12 @@ namespace KCM
{ {
if (KCClient.client.IsConnected) if (KCClient.client.IsConnected)
{ {
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket")) if (ShouldSuppressVillagerTeleportPackets)
return;
bool calledFromPacket = false;
try { calledFromPacket = PacketHandler.IsHandlingPacket; } catch { }
if (calledFromPacket)
return; return;
new VillagerTeleportTo() new VillagerTeleportTo()
@@ -1591,18 +1668,13 @@ namespace KCM
public static bool Prefix(ref string __result) public static bool Prefix(ref string __result)
{ {
Main.helper.Log("Get save dir"); Main.helper.Log("Get save dir");
if (KCClient.client.IsConnected) if (KCServer.IsRunning)
{ {
if (KCServer.IsRunning)
{
}
__result = Application.persistentDataPath + "/Saves/Multiplayer"; __result = Application.persistentDataPath + "/Saves/Multiplayer";
return false; return false;
} }
__result = Application.persistentDataPath + "/Saves"; ; __result = Application.persistentDataPath + "/Saves";
return true; return true;
} }
} }
@@ -1682,6 +1754,14 @@ namespace KCM
Main.SetMultiplayerSaveLoadInProgress(false); Main.SetMultiplayerSaveLoadInProgress(false);
} }
try
{
RunPostLoadRebuild("LoadAtPath (multiplayer)");
}
catch
{
}
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
} }

View File

@@ -81,7 +81,7 @@ namespace KCM.Packets.Game
public override void HandlePacketServer() public override void HandlePacketServer()
{ {
// Server doesn't need to handle this packet // Server relay is handled automatically by PacketHandler unless [NoServerRelay] is used.
} }
} }
} }

View File

@@ -15,6 +15,14 @@ namespace KCM.Packets.Handlers
{ {
public class PacketHandler public class PacketHandler
{ {
[ThreadStatic]
private static bool isHandlingPacket;
public static bool IsHandlingPacket
{
get { return isHandlingPacket; }
}
public static Dictionary<ushort, PacketRef> Packets = new Dictionary<ushort, PacketRef>(); public static Dictionary<ushort, PacketRef> Packets = new Dictionary<ushort, PacketRef>();
public class PacketRef public class PacketRef
{ {
@@ -183,6 +191,7 @@ namespace KCM.Packets.Handlers
{ {
try try
{ {
isHandlingPacket = true;
packet.HandlePacketClient(); packet.HandlePacketClient();
} }
catch (Exception ex) catch (Exception ex)
@@ -205,6 +214,10 @@ namespace KCM.Packets.Handlers
Main.helper.Log(ex.InnerException.StackTrace); Main.helper.Log(ex.InnerException.StackTrace);
} }
} }
finally
{
isHandlingPacket = false;
}
} }
/* if (PacketHandlers.TryGetValue(id, out PacketHandlerDelegate handler)) /* if (PacketHandlers.TryGetValue(id, out PacketHandlerDelegate handler))

View File

@@ -117,6 +117,14 @@ namespace KCM.Packets.Lobby
{ {
Main.SetMultiplayerSaveLoadInProgress(false); Main.SetMultiplayerSaveLoadInProgress(false);
} }
try
{
RunPostLoadRebuild("Save transfer complete");
}
catch
{
}
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
try try

View File

@@ -25,6 +25,7 @@ namespace KCM.Packets.Lobby
try try
{ {
int desiredSpeed = 1;
if (!LobbyManager.loadingSave) if (!LobbyManager.loadingSave)
{ {
SpeedControlUI.inst.SetSpeed(0); SpeedControlUI.inst.SetSpeed(0);
@@ -39,31 +40,13 @@ namespace KCM.Packets.Lobby
Main.helper.Log(ex.ToString()); Main.helper.Log(ex.ToString());
} }
try SpeedControlUI.inst.SetSpeed(desiredSpeed);
{
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 else
{ {
LobbyManager.loadingSave = false; LobbyManager.loadingSave = false;
GameState.inst.SetNewMode(GameState.inst.playingMode); GameState.inst.SetNewMode(GameState.inst.playingMode);
SpeedControlUI.inst.SetSpeed(desiredSpeed);
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -23,27 +23,72 @@ namespace KCM.StateManagement.BuildingState
{ {
try try
{ {
Observer observer = (Observer)sender; Observer observer = sender as Observer;
if (observer == null)
return;
Building building = (Building)observer.state; Building building = observer.state as Building;
if (building == null)
return;
//Main.helper.Log("Should send building network update for: " + building.UniqueName); //Main.helper.Log("Should send building network update for: " + building.UniqueName);
var t = building.transform;
if (t == null)
return;
Quaternion rotation = t.rotation;
Vector3 globalPosition = t.position;
Vector3 localPosition = t.localPosition;
if (t.childCount > 0)
{
try
{
var child = t.GetChild(0);
if (child != null)
{
rotation = child.rotation;
localPosition = child.localPosition;
}
}
catch
{
}
}
float resourceProgress = 0f;
try
{
var field = building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
object value = field.GetValue(building);
if (value is float)
resourceProgress = (float)value;
else if (value != null)
resourceProgress = Convert.ToSingle(value);
}
}
catch
{
}
new BuildingStatePacket() new BuildingStatePacket()
{ {
customName = building.customName, customName = building.customName,
guid = building.guid, guid = building.guid,
uniqueName = building.UniqueName, uniqueName = building.UniqueName,
rotation = building.transform.GetChild(0).rotation, rotation = rotation,
globalPosition = building.transform.position, globalPosition = globalPosition,
localPosition = building.transform.GetChild(0).localPosition, localPosition = localPosition,
built = building.IsBuilt(), built = building.IsBuilt(),
placed = building.IsPlaced(), placed = building.IsPlaced(),
open = building.Open, open = building.Open,
doBuildAnimation = building.doBuildAnimation, doBuildAnimation = building.doBuildAnimation,
constructionPaused = building.constructionPaused, constructionPaused = building.constructionPaused,
constructionProgress = building.constructionProgress, constructionProgress = building.constructionProgress,
resourceProgress = (float)building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(building), resourceProgress = resourceProgress,
life = building.Life, life = building.Life,
ModifiedMaxLife = building.ModifiedMaxLife, ModifiedMaxLife = building.ModifiedMaxLife,
yearBuilt = building.YearBuilt, yearBuilt = building.YearBuilt,

View File

@@ -128,6 +128,31 @@ namespace KCM.StateManagement.Observers
if (this.state == null) if (this.state == null)
return; return;
// Unity uses "fake null" for destroyed objects. Since our state is stored as object,
// we must explicitly detect that case to avoid exceptions + log spam.
try
{
UnityEngine.Object unityObj = this.state as UnityEngine.Object;
if (this.state is UnityEngine.Object && unityObj == null)
{
try { StateObserver.observers.Remove(this.state.GetHashCode()); } catch { }
try
{
if (observerObject != null)
UnityEngine.Object.Destroy(observerObject);
else
UnityEngine.Object.Destroy(this.gameObject);
}
catch
{
}
return;
}
}
catch
{
}
if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds); if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds);
return; return;