394 lines
14 KiB
C#
394 lines
14 KiB
C#
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<string, KCPlayer> kCPlayers = new Dictionary<string, KCPlayer>();
|
|
public static Dictionary<ushort, string> clientSteamIds = new Dictionary<ushort, string>();
|
|
|
|
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<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";
|
|
|
|
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<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
|
|
}
|
|
} |