580 lines
20 KiB
C#
580 lines
20 KiB
C#
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<string, KCPlayer> kCPlayers = new Dictionary<string, KCPlayer>();
|
|
public static Dictionary<ushort, string> clientSteamIds = new Dictionary<ushort, string>();
|
|
|
|
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<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>();
|
|
|
|
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<MethodBase> TargetMethods()
|
|
{
|
|
var meth = typeof(SteamManager).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
|
return meth.Cast<MethodBase>();
|
|
}
|
|
|
|
public static bool Prefix(MethodBase __originalMethod)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|