Files
K-C-Multiplayer/Main.cs

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
}
}