using Assets.Code; using HarmonyLib; 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 Riptide.Utils; using Steamworks; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; namespace KCM { public class Main : MonoBehaviour { public static Harmony harmony = new Harmony("com.kcm.mod"); public static Helper helper; public static Dictionary kCPlayers = new Dictionary(); public static Dictionary clientSteamIds = new Dictionary(); public static SteamClient steamClient; public static SteamServer steamServer; public static string PlayerSteamID => SteamUser.GetSteamID().ToString(); void Awake() { helper = new Helper("KCM", true, true, true); helper.Log("KCM Awake"); } void Start() { helper.Log("KCM Start"); harmony.PatchAll(); } 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 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) { helper?.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) { helper?.Log(e.ToString()); } try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { helper?.Log(e.ToString()); } SetLoadTickDelay(Player.inst, 1); SetLoadTickDelay(UnitSystem.inst, 1); SetLoadTickDelay(JobSystem.inst, 1); SetLoadTickDelay(VillagerSystem.inst, 1); } catch (Exception e) { helper?.Log("Post-load rebuild failed"); helper?.Log(e.ToString()); } } 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"; helper.Log("Failed finding player by teamID: " + teamId + " My teamID is: " + myTeamId); helper.Log(kCPlayers.Count.ToString()); helper.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) { helper.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) { helper.Log(e.ToString()); helper.Log(e.Message); helper.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) { helper.Log(e.ToString()); helper.Log(e.Message); helper.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; helper.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 helper.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 } }