using Assets.Code; using Harmony; using KCM.Packets; using KCM.Packets.Game; using KCM.Packets.Game.GamePlayer; using KCM.Packets.Lobby; using KCM.Packets.Network; using KCM.Packets.State; using KCM.StateManagement.Observers; using KCM.StateManagement.Sync; using KCM.StateManagement.BuildingState; using Riptide; using Steamworks; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; using SteamTransportClient = Riptide.Transports.Steam.SteamClient; using SteamTransportServer = Riptide.Transports.Steam.SteamServer; namespace KCM { public class Main : MonoBehaviour { public static HarmonyInstance harmony; public static KCModHelper helper; public static Dictionary kCPlayers = new Dictionary(); public static Dictionary clientSteamIds = new Dictionary(); public static SteamTransportClient steamClient; public static SteamTransportServer steamServer; public static string PlayerSteamID => SteamUser.GetSteamID().ToString(); void Awake() { Log("KCM Awake"); } void Start() { Log("KCM Start"); EnsureNetworking(); if (harmony == null) harmony = HarmonyInstance.Create("com.kcm.mod"); harmony.PatchAll(Assembly.GetExecutingAssembly()); } public static void EnsureNetworking() { if (steamServer == null) steamServer = new SteamTransportServer(); if (steamClient == null) steamClient = new SteamTransportClient(steamServer); else steamClient.ChangeLocalServer(steamServer); if (KCClient.client != null) KCClient.client.ChangeTransport(steamClient); if (KCServer.server != null) KCServer.server.ChangeTransport(steamServer); } public static void Log(string message) { if (helper != null) helper.Log(message ?? string.Empty); else Debug.Log(message ?? string.Empty); } public static void Log(Exception ex) { Log(ex != null ? ex.ToString() : string.Empty); } public static void ServerUpdate() { if (!KCServer.IsRunning) return; SyncManager.ServerUpdate(); } public static void ClientUpdate() { if (!KCClient.client.IsConnected) return; // Handle client-side updates } public static void TransitionTo(string scene) { // Simple scene transition UnityEngine.SceneManagement.SceneManager.LoadScene(scene); } public static void ResetMultiplayerState() { // Reset multiplayer state kCPlayers.Clear(); clientSteamIds.Clear(); } public static void ResetMultiplayerState(string reason) { if (!string.IsNullOrEmpty(reason)) Log("ResetMultiplayerState: " + reason); ResetMultiplayerState(); } public static void SetMultiplayerSaveLoadInProgress(bool inProgress) { // Set save/load progress flag // Implementation depends on UI } public static int FixedUpdateInterval = 0; private void FixedUpdate() { // send batched building placement info /*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0)) { foreach (Building building in PlaceHook.QueuedBuildings) { new WorldPlace() { uniqueName = building.UniqueName, customName = building.customName, guid = building.guid, rotation = building.transform.GetChild(0).rotation, globalPosition = building.transform.position, localPosition = building.transform.GetChild(0).localPosition, built = building.IsBuilt(), placed = building.IsPlaced(), open = building.Open, doBuildAnimation = building.doBuildAnimation, constructionPaused = building.constructionPaused, constructionProgress = building.constructionProgress, life = building.Life, ModifiedMaxLife = building.ModifiedMaxLife, yearBuilt = building.YearBuilt, decayProtection = building.decayProtection, seenByPlayer = building.seenByPlayer }.Send(); } PlaceHook.QueuedBuildings.Clear(); }*/ FixedUpdateInterval++; // Force AI updates in multiplayer every few frames if (KCClient.client.IsConnected && FixedUpdateInterval % 60 == 0 && Time.timeScale > 0) { ForceMultiplayerAIUpdate(); } } private static void ForceMultiplayerAIUpdate() { try { foreach (var player in kCPlayers.Values) { if (player?.inst == null) continue; // Force villager AI updates using reflection for (int i = 0; i < player.inst.Workers.Count; i++) { Villager v = player.inst.Workers.data[i]; if (v != null) { try { // Use reflection to access brain and call Think() var brainField = typeof(Villager).GetField("brain", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var brain = brainField?.GetValue(v); if (brain != null) { var thinkMethod = brain.GetType().GetMethod("Think", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); thinkMethod?.Invoke(brain, null); } } catch { } } } // Force homeless to find jobs using reflection for (int i = 0; i < player.inst.Homeless.Count; i++) { Villager v = player.inst.Homeless.data[i]; if (v != null) { try { // Check if villager has no job var workerJobField = typeof(Villager).GetField("workerJob", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var workerJob = workerJobField?.GetValue(v); if (workerJob == null && JobSystem.inst != null) { // Try to assign job var tryAssignMethod = typeof(JobSystem).GetMethod("TryAssignJob", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); tryAssignMethod?.Invoke(JobSystem.inst, new object[] { v }); } } catch { } } } } } catch (Exception e) { Log("Error in ForceMultiplayerAIUpdate: " + e.Message); } } public static void SetLoadTickDelay(object instance, int ticks) { if (instance == null) return; try { FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic); if (loadTickDelayField != null) loadTickDelayField.SetValue(instance, ticks); } catch { } } public static void RunPostLoadRebuild() { try { try { Player.inst.irrigation.UpdateIrrigation(); } catch (Exception e) { Log(e); } try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { Log(e); } SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1); SetLoadTickDelay(VillagerSystem.inst, 1); } catch (Exception e) { Log("Post-load rebuild failed"); Log(e); } } public static Player GetPlayerByTeamID(int teamId) { KCPlayer match = kCPlayers.Values.FirstOrDefault(p => p != null && p.inst != null && p.inst.PlayerLandmassOwner != null && p.inst.PlayerLandmassOwner.teamId == teamId); if (match == null) { long lastTime = 0; Dictionary lastTeamIdLookupLogMs = new Dictionary(); if (!lastTeamIdLookupLogMs.TryGetValue(teamId, out lastTime) || (DateTimeOffset.Now.ToUnixTimeMilliseconds() - lastTime) >= 2000) { lastTeamIdLookupLogMs[teamId] = DateTimeOffset.Now.ToUnixTimeMilliseconds(); string myTeamId = (Player.inst != null && Player.inst.PlayerLandmassOwner != null) ? Player.inst.PlayerLandmassOwner.teamId.ToString() : "unknown"; Log("Failed finding player by teamID: " + teamId + " My teamID is: " + myTeamId); Log(kCPlayers.Count.ToString()); Log(string.Join(", ", kCPlayers.Values.Where(p => p != null && p.inst != null && p.inst.PlayerLandmassOwner != null).Select(p => p.inst.PlayerLandmassOwner.teamId.ToString()))); } } return match?.inst; } public static Player GetPlayerByBuilding(Building building) { try { return GetPlayerByTeamID(building.TeamID()); } catch { return Player.inst; } } #region "Building Hooks" [HarmonyPatch(typeof(Building), "CompleteBuild")] public class BuildingCompleteBuildHook { public static bool Prefix(Building __instance, ref bool __result) { try { if (KCClient.client.IsConnected) { if (__instance.TeamID() == Player.inst.PlayerLandmassOwner.teamId) { Log("Overridden complete build"); Player player = GetPlayerByTeamID(__instance.TeamID()); typeof(Building).GetField("built", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, true); __instance.UpdateMaterialSelection(); __instance.SendMessage("OnBuilt", SendMessageOptions.DontRequireReceiver); typeof(Building).GetField("yearBuilt", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, player.CurrYear); typeof(Building).GetMethod("AddAllResourceProviders", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(__instance, null); player.BuildingNowBuilt(__instance); typeof(Building).GetMethod("TryAddJobs", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(__instance, null); __instance.BakePathing(); return false; } } } catch (Exception e) { Log(e.ToString()); Log(e.Message); Log(e.StackTrace); } return true; } } [HarmonyPatch(typeof(Building), "UpdateConstruction")] public class BuildingUpdateHook { public static void Prefix(Building __instance) { try { if (KCClient.client.IsConnected) { if (__instance.TeamID() == Player.inst.PlayerLandmassOwner.teamId) StateObserver.RegisterObserver(__instance, new string[] { "customName", "guid", "UniqueName", "built", "placed", "open", "doBuildAnimation", "constructionPaused", "constructionProgress", "resourceProgress", "Life", "ModifiedMaxLife", "CollectForBuild", "yearBuilt", "decayProtection", "seenByPlayer", }, BuildingStateManager.BuildingStateChanged, BuildingStateManager.SendBuildingUpdate); } } catch (Exception e) { Log(e.ToString()); Log(e.Message); Log(e.StackTrace); } } } #endregion #region "Time Hooks" [HarmonyPatch(typeof(SpeedControlUI), "SetSpeed")] public class SpeedControlUISetSpeedHook { public static void Postfix(int idx) { if (!KCClient.client.IsConnected) return; Log("SpeedControlUI.SetSpeed (local): " + idx); // Force AI restart when speed changes from paused to playing if (idx > 0) { ForceMultiplayerAIUpdate(); } bool isPaused = (idx == 0); new SetSpeed() { speed = idx, isPaused = isPaused }.Send(); } } // Force AI restart when game is unpaused [HarmonyPatch(typeof(SpeedControlUI), "SetPaused")] public class SpeedControlUISetPausedHook { public static void Postfix(bool paused) { if (!KCClient.client.IsConnected) return; if (!paused) { // Game is unpaused, force AI restart Log("Game unpaused, forcing AI restart"); ForceMultiplayerAIUpdate(); } } } #endregion #region "SteamManager Hook" [HarmonyPatch] public class SteamManagerAwakeHook { static IEnumerable TargetMethods() { var meth = typeof(SteamManager).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); return meth.Cast(); } public static bool Prefix(MethodBase __originalMethod) { return false; } } #endregion } }