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 KCM.Enums; using Riptide; using Steamworks; using System; using System.Collections.Generic; 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(); // K&C mod loader entrypoint public void Preload(KCModHelper modHelper) { helper = modHelper; try { EnsureNetworking(); if (harmony == null) harmony = HarmonyInstance.Create("com.kcm.mod"); harmony.PatchAll(Assembly.GetExecutingAssembly()); try { new PrefabManager().PreScriptLoad(modHelper); } catch (Exception ex) { Log("Prefab preload failed"); Log(ex); } } catch (Exception ex) { Log("Preload failed"); Log(ex); } } 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 UnityEngine.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 TransitionTo(MenuState menuState) { try { if (GameState.inst != null && GameState.inst.mainMenuMode != null) { GameState.inst.SetNewMode(GameState.inst.mainMenuMode); SetMainMenuState(GameState.inst.mainMenuMode, (int)menuState); return; } } catch (Exception ex) { Log(ex); } TransitionTo(menuState.ToString()); } private static void SetMainMenuState(object mainMenuMode, int menuStateValue) { if (mainMenuMode == null) return; Type type = mainMenuMode.GetType(); const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; string[] methodNames = new string[] { "SetMenuState", "SetState", "GoToState", "GotoState", "TransitionToState", "TransitionTo", }; MethodInfo[] methods = type.GetMethods(flags); for (int i = 0; i < methodNames.Length; i++) { string name = methodNames[i]; for (int j = 0; j < methods.Length; j++) { MethodInfo method = methods[j]; if (method == null || method.Name != name) continue; ParameterInfo[] parameters = method.GetParameters(); if (parameters == null || parameters.Length != 1) continue; Type paramType = parameters[0].ParameterType; if (paramType == typeof(int)) { method.Invoke(mainMenuMode, new object[] { menuStateValue }); return; } if (paramType != null && paramType.IsEnum) { object enumValue = Enum.ToObject(paramType, menuStateValue); method.Invoke(mainMenuMode, new object[] { enumValue }); return; } } } FieldInfo[] fields = type.GetFields(flags); for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (field == null) continue; if (field.FieldType == typeof(int)) { field.SetValue(mainMenuMode, menuStateValue); return; } if (field.FieldType != null && field.FieldType.IsEnum) { field.SetValue(mainMenuMode, Enum.ToObject(field.FieldType, menuStateValue)); return; } } PropertyInfo[] properties = type.GetProperties(flags); for (int i = 0; i < properties.Length; i++) { PropertyInfo prop = properties[i]; if (prop == null || !prop.CanWrite) continue; if (prop.PropertyType == typeof(int)) { prop.SetValue(mainMenuMode, menuStateValue, null); return; } if (prop.PropertyType != null && prop.PropertyType.IsEnum) { prop.SetValue(mainMenuMode, Enum.ToObject(prop.PropertyType, menuStateValue), null); return; } } Log($"TransitionTo(MenuState) could not set state value={menuStateValue} on {type.FullName}"); } 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 } }