Files
K-C-Multiplayer/Main.cs

549 lines
19 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();
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
}
}