From 734dbc721d6d7bbfa028022003fed6a2f2845d0e Mon Sep 17 00:00:00 2001 From: mkiplm-svg Date: Mon, 19 Oct 2026 09:23:03 -0400 Subject: [PATCH 1/3] Add experimental RimWorld 1.7 support --- Source/Client/Desyncs/DeferredStackTracing.cs | 303 +-- Source/Client/Desyncs/JittedMethods.cs | 214 +- Source/Client/Desyncs/SaveableDesyncInfo.cs | 398 ++-- Source/Client/Desyncs/StackTraceLogItem.cs | 173 +- Source/Client/Factions/MultifactionPatches.cs | 1918 ++++++++--------- Source/Client/Multiplayer.cs | 509 ++--- Source/Client/Multiplayer.csproj | 169 +- .../Patches/GravshipTravelSessionPatches.cs | 514 ++--- Source/Client/Patches/VanillaTweaks.cs | 296 +-- Source/Client/Syncing/SyncTemplates.cs | 206 +- Source/Client/UI/MainMenuAnimation.cs | 879 ++++---- Source/Client/Util/MpUI.cs | 548 ++--- Source/Client/Util/MpUtil.cs | 403 ++-- Source/Common/Common.csproj | 85 +- Source/Common/Native.cs | 532 ++--- Source/Directory.Build.targets | 26 + .../MultiplayerLoader.csproj | 75 +- Source/MultiplayerLoader/Prepatches.cs | 160 +- Source/MultiplayerLoader/SyncRituals.cs | 276 +-- Source/Tests/CoreClrRuntimeTest.cs | 17 + Source/Tests/Tests.csproj | 65 +- .../ClientInitDataPacket.verified.txt | 2 +- .../ServerJoinDataPacket.verified.txt | 2 +- 23 files changed, 3932 insertions(+), 3838 deletions(-) create mode 100644 Source/Directory.Build.targets create mode 100644 Source/Tests/CoreClrRuntimeTest.cs diff --git a/Source/Client/Desyncs/DeferredStackTracing.cs b/Source/Client/Desyncs/DeferredStackTracing.cs index db53da2df..f36c9395f 100644 --- a/Source/Client/Desyncs/DeferredStackTracing.cs +++ b/Source/Client/Desyncs/DeferredStackTracing.cs @@ -1,146 +1,169 @@ using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; -using HarmonyLib; -using Multiplayer.Client.Patches; -using Multiplayer.Common; -using RimWorld; -using Verse; -using Verse.AI; - -namespace Multiplayer.Client.Desyncs -{ - [EarlyPatch] - [HarmonyPatch] - public static class DeferredStackTracing - { - public static int ignoreTraces; - public static long maxTraceDepth; - public static int randCalls; - - static IEnumerable TargetMethods() - { - yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Value)); - yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Int)); - } - - public static int acc; - - private static bool SupportsDeferredStackTracing => - RuntimeInformation.ProcessArchitecture is Architecture.X64 or Architecture.X86; - +using System.Diagnostics; +using System.Text; +using System.Reflection; +using System.Runtime.InteropServices; +using HarmonyLib; +using Multiplayer.Client.Patches; +using Multiplayer.Common; +using RimWorld; +using Verse; +using Verse.AI; + +namespace Multiplayer.Client.Desyncs +{ + [EarlyPatch] + [HarmonyPatch] + public static class DeferredStackTracing + { + public static int ignoreTraces; + public static long maxTraceDepth; + public static int randCalls; + + static IEnumerable TargetMethods() + { + yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Value)); + yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Int)); + } + + public static int acc; + + private static bool SupportsDeferredStackTracing => + RuntimeInformation.ProcessArchitecture is Architecture.X64 or Architecture.X86; + public static void Postfix() { - if (Native.LmfPtr == 0) return; + if (Native.IsMonoRuntime && Native.LmfPtr == 0) return; if (!ShouldAddStackTraceForDesyncLog()) return; var logItem = StackTraceLogItemRaw.GetFromPool(); - var trace = logItem.raw; - int hash = 0; - // Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value. - int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash, skipFrames: 1); - - Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); - - acc++; - } - - public static bool ShouldAddStackTraceForDesyncLog() - { - if (!SupportsDeferredStackTracing) return false; - if (Multiplayer.Client == null) return false; - if (Multiplayer.settings.desyncTracingMode == DesyncTracingMode.None) return false; - if (Multiplayer.game == null) return false; - - if (!Multiplayer.game.gameComp.logDesyncTraces) return false; - - if (Rand.stateStack.Count > 1) return false; - if (Multiplayer.IsReplay) return false; - - if (!Multiplayer.Ticking && !Multiplayer.ExecutingCmds) return false; - - return ignoreTraces == 0; - } - } - - [HarmonyPatch(typeof(UniqueIDsManager), nameof(UniqueIDsManager.GetNextID))] - public static class UniqueIdsPatch - { - static void Postfix() - { - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Thing), nameof(Thing.SpawnSetup))] - public static class ThingSpawnPatch - { - static void Postfix(Thing __instance) - { - if (__instance.def.HasThingIDNumber) - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] - public static class ThingDeSpawnPatch - { - static void Postfix(Thing __instance) - { - if (__instance.def.HasThingIDNumber) - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] - public static class EndCurrentJobPatch - { - static void Prefix(Pawn_JobTracker __instance) - { - if (MpVersion.IsDebug && __instance.curJob != null && DeferredStackTracing.ShouldAddStackTraceForDesyncLog()) - Multiplayer.game.sync.TryAddInfoForDesyncLog($"EndCurrentJob for {__instance.pawn}: {__instance.curJob}", ""); - } - } - - [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] - static class WildAnimalSpawnerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(WildPlantSpawner), nameof(WildPlantSpawner.WildPlantSpawnerTick))] - static class WildPlantSpawnerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(SteadyEnvironmentEffects), nameof(SteadyEnvironmentEffects.SteadyEnvironmentEffectsTick))] - static class SteadyEnvironmentEffectsTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(TempTerrainManager), nameof(TempTerrainManager.Tick))] - static class TempTerrainManagerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - /*[HarmonyPatch(typeof(StoreUtility), nameof(StoreUtility.TryFindBestBetterStoreCellForWorker))] - static class FindBestStorageCellTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - }*/ - - [HarmonyPatch(typeof(IntermittentSteamSprayer), nameof(IntermittentSteamSprayer.SteamSprayerTick))] - static class SteamSprayerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } -} + if (!Native.IsMonoRuntime) + { + // Managed fallback: never interpret CoreCLR addresses as Mono frames. + var frames = new StackTrace(1, false).GetFrames(); + var text = new StringBuilder(); + int managedHash = 17; + int managedDepth = System.Math.Min(frames?.Length ?? 0, StackTraceLogItemRaw.MaxDepth); + for (int i = 0; i < managedDepth; i++) + { + var method = frames[i].GetMethod(); + var name = $"{method?.DeclaringType?.FullName}.{method}"; + text.AppendLine(name); + // Explicit stable hashing; no process-randomized string hashes. + foreach (char c in name) + managedHash = unchecked(managedHash * 31 + c); + } + logItem.managedTrace = text.ToString(); + Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, managedDepth, managedHash); + acc++; + return; + } + var trace = logItem.raw; + int hash = 0; + // Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value. + int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash, skipFrames: 1); + + Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); + + acc++; + } + + public static bool ShouldAddStackTraceForDesyncLog() + { + if (!SupportsDeferredStackTracing) return false; + if (Multiplayer.Client == null) return false; + if (Multiplayer.settings.desyncTracingMode == DesyncTracingMode.None) return false; + if (Multiplayer.game == null) return false; + + if (!Multiplayer.game.gameComp.logDesyncTraces) return false; + + if (Rand.stateStack.Count > 1) return false; + if (Multiplayer.IsReplay) return false; + + if (!Multiplayer.Ticking && !Multiplayer.ExecutingCmds) return false; + + return ignoreTraces == 0; + } + } + + [HarmonyPatch(typeof(UniqueIDsManager), nameof(UniqueIDsManager.GetNextID))] + public static class UniqueIdsPatch + { + static void Postfix() + { + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.SpawnSetup))] + public static class ThingSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] + public static class ThingDeSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] + public static class EndCurrentJobPatch + { + static void Prefix(Pawn_JobTracker __instance) + { + if (MpVersion.IsDebug && __instance.curJob != null && DeferredStackTracing.ShouldAddStackTraceForDesyncLog()) + Multiplayer.game.sync.TryAddInfoForDesyncLog($"EndCurrentJob for {__instance.pawn}: {__instance.curJob}", ""); + } + } + + [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] + static class WildAnimalSpawnerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(WildPlantSpawner), nameof(WildPlantSpawner.WildPlantSpawnerTick))] + static class WildPlantSpawnerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(SteadyEnvironmentEffects), nameof(SteadyEnvironmentEffects.SteadyEnvironmentEffectsTick))] + static class SteadyEnvironmentEffectsTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(TempTerrainManager), nameof(TempTerrainManager.Tick))] + static class TempTerrainManagerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + /*[HarmonyPatch(typeof(StoreUtility), nameof(StoreUtility.TryFindBestBetterStoreCellForWorker))] + static class FindBestStorageCellTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + }*/ + + [HarmonyPatch(typeof(IntermittentSteamSprayer), nameof(IntermittentSteamSprayer.SteamSprayerTick))] + static class SteamSprayerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } +} diff --git a/Source/Client/Desyncs/JittedMethods.cs b/Source/Client/Desyncs/JittedMethods.cs index 7928b04b4..7b966c1a4 100644 --- a/Source/Client/Desyncs/JittedMethods.cs +++ b/Source/Client/Desyncs/JittedMethods.cs @@ -1,112 +1,116 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using HarmonyLib; -using JetBrains.Annotations; -using Verse; - -namespace Multiplayer.Client.Desyncs; - -public class JittedMethod -{ - [CanBeNull] public MethodBase method; - [CanBeNull] public MethodBase from; - [CanBeNull] public int[] mapTicks; - public int worldTicks; - public int timer; - public bool inInterface; - - public string TimeString() - { - return $"m:{mapTicks?.Join(delimiter: ",") ?? "[]"} w:{worldTicks} t:{timer}"; - } - - public override string ToString() - { - return $"{method?.DeclaringType}.{method?.Name} from {from?.DeclaringType}.{from?.Name} {TimeString()} i:{inInterface}"; - } -} - -public static class JittedMethods -{ - private static Queue methodQueue = new(); - private static IntPtr profiler; - private static bool adding; - +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; +using JetBrains.Annotations; +using Verse; + +namespace Multiplayer.Client.Desyncs; + +public class JittedMethod +{ + [CanBeNull] public MethodBase method; + [CanBeNull] public MethodBase from; + [CanBeNull] public int[] mapTicks; + public int worldTicks; + public int timer; + public bool inInterface; + + public string TimeString() + { + return $"m:{mapTicks?.Join(delimiter: ",") ?? "[]"} w:{worldTicks} t:{timer}"; + } + + public override string ToString() + { + return $"{method?.DeclaringType}.{method?.Name} from {from?.DeclaringType}.{from?.Name} {TimeString()} i:{inInterface}"; + } +} + +public static class JittedMethods +{ + private static Queue methodQueue = new(); + private static IntPtr profiler; + private static bool adding; + public static void Init() { - profiler = Native.mono_profiler_create(IntPtr.Zero); - Native.mono_profiler_set_jit_done_callback(profiler, (_, method, _) => - { - if (adding) return; - - try - { - adding = true; - - if (UnityData.IsInMainThread && Multiplayer.settings != null && Native.mono_method_get_token(method) != 0) - { - var methodBase = Native.GetMethodBaseFromRuntimePointer(method); - methodQueue.Enqueue(new JittedMethod - { - method = methodBase, - from = new StackTrace().GetFrame(1)?.GetMethod(), - mapTicks = Multiplayer.game?.asyncTimeComps.Select(c => c.mapTicks).ToArray(), - worldTicks = Multiplayer.game?.asyncWorldTimeComp?.worldTicks ?? -1, - timer = TickPatch.Timer, - inInterface = Multiplayer.InInterface - }); - - if (methodQueue.Count > Multiplayer.settings.jittedMethodsInDesync) - methodQueue.Dequeue(); - } - } - finally - { - adding = false; - } - }); - } - + if (!Native.IsMonoRuntime) return; + profiler = Native.mono_profiler_create(IntPtr.Zero); + Native.mono_profiler_set_jit_done_callback(profiler, (_, method, _) => + { + if (adding) return; + + try + { + adding = true; + + if (UnityData.IsInMainThread && Multiplayer.settings != null && Native.mono_method_get_token(method) != 0) + { + var methodBase = Native.GetMethodBaseFromRuntimePointer(method); + methodQueue.Enqueue(new JittedMethod + { + method = methodBase, + from = new StackTrace().GetFrame(1)?.GetMethod(), + mapTicks = Multiplayer.game?.asyncTimeComps.Select(c => c.mapTicks).ToArray(), + worldTicks = Multiplayer.game?.asyncWorldTimeComp?.worldTicks ?? -1, + timer = TickPatch.Timer, + inInterface = Multiplayer.InInterface + }); + + if (methodQueue.Count > Multiplayer.settings.jittedMethodsInDesync) + methodQueue.Dequeue(); + } + } + finally + { + adding = false; + } + }); + } + public static void OnApplicationQuit() { + if (!Native.IsMonoRuntime || profiler == IntPtr.Zero) return; Native.mono_profiler_set_jit_done_callback(profiler, null); - } - + } + public static string GetJittedMethodsString() { - // Two ToArrays to prevent a compilation in between from causing a "Collection was modified" exception - var jittedMethods = methodQueue.ToArray().Select((j, i) => (j, i)).ToArray(); - - var builder = new StringBuilder(); - builder.Append("In simulation:"); // This will get a newline from the != comparison below - var timeString = ""; - - foreach (var j in jittedMethods.Where(j => !j.j.inInterface)) - { - if (timeString != j.j.TimeString()) - builder.AppendLine(); - builder.AppendLine($"{j.i} {j.j}"); - timeString = j.j.TimeString(); - } - - builder.AppendLine(); - builder.AppendLine(); - - timeString = ""; - - builder.Append("In interface:"); - foreach (var j in jittedMethods.Where(j => j.j.inInterface)) - { - if (timeString != j.j.TimeString()) - builder.AppendLine(); - builder.AppendLine($"{j.i} {j.j}"); - timeString = j.j.TimeString(); - } - - return builder.ToString(); - } -} + if (!Native.IsMonoRuntime) + return "Mono JIT history is unavailable on this runtime."; + // Two ToArrays to prevent a compilation in between from causing a "Collection was modified" exception + var jittedMethods = methodQueue.ToArray().Select((j, i) => (j, i)).ToArray(); + + var builder = new StringBuilder(); + builder.Append("In simulation:"); // This will get a newline from the != comparison below + var timeString = ""; + + foreach (var j in jittedMethods.Where(j => !j.j.inInterface)) + { + if (timeString != j.j.TimeString()) + builder.AppendLine(); + builder.AppendLine($"{j.i} {j.j}"); + timeString = j.j.TimeString(); + } + + builder.AppendLine(); + builder.AppendLine(); + + timeString = ""; + + builder.Append("In interface:"); + foreach (var j in jittedMethods.Where(j => j.j.inInterface)) + { + if (timeString != j.j.TimeString()) + builder.AppendLine(); + builder.AppendLine($"{j.i} {j.j}"); + timeString = j.j.TimeString(); + } + + return builder.ToString(); + } +} diff --git a/Source/Client/Desyncs/SaveableDesyncInfo.cs b/Source/Client/Desyncs/SaveableDesyncInfo.cs index 200c3dfee..2bc80eba8 100644 --- a/Source/Client/Desyncs/SaveableDesyncInfo.cs +++ b/Source/Client/Desyncs/SaveableDesyncInfo.cs @@ -1,199 +1,201 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using HarmonyLib; -using JetBrains.Annotations; -using Multiplayer.Common; -using Multiplayer.Common.Util; -using RimWorld; -using UnityEngine; -using Verse; -using CompressionLevel = System.IO.Compression.CompressionLevel; - -namespace Multiplayer.Client.Desyncs; - -public class SaveableDesyncInfo( - SyncCoordinator coordinator, - ClientSyncOpinion local, - ClientSyncOpinion remote, - int diffAt, - bool diffAtFound) -{ - public readonly ClientSyncOpinion local = local; - public readonly ClientSyncOpinion remote = remote; - public readonly int diffAt = diffAt; - private readonly Task metadata = Task.Run(MetadataGenerator.Generate); - private readonly Task replay = Task.Run(SaveReplayIfApplicable); - - public bool ReadyToSave => metadata.IsCompleted && replay.IsCompleted; - - public void Save([CanBeNull] HostInfo hostInfo) - { - var watch = Stopwatch.StartNew(); - - try - { - var desyncFilePath = FindFileNameForNextDesyncFile(); - using var zip = MpZipFile.Open(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip"), ZipArchiveMode.Create); - - zip.AddEntry("desync_info", GetDesyncDetails()); - - zip.AddEntry("local_traces.txt", GetLocalTraces()); - zip.AddEntry("host_traces.txt", hostInfo?.Traces ?? "No host traces"); - - zip.AddEntry("local_jitted_methods.txt", JittedMethods.GetJittedMethodsString()); - zip.AddEntry("host_jitted_methods.txt", hostInfo?.JittedMethods ?? "No host jitted methods"); - - var extraLogs = LogGenerator.PrepareLogData(); - if (extraLogs != null) zip.AddEntry("local_logs.txt", extraLogs); - - zip.AddEntry("local_metadata.txt", metadata.Result); - - try - { - replay.Wait(); - } - catch (AggregateException e) - { - if (e.InnerExceptions.SingleOrDefault(inner => inner is TaskCanceledException) == null) throw; - } - if (replay.IsCompletedSuccessfully) { - var replayFile = replay.Result; - zip.CreateEntryFromFile(replayFile.FullName, "replay.rwmts", CompressionLevel.NoCompression); - DeleteFileSilent(replayFile); - } - } - catch (Exception e) - { - Log.Error($"Exception writing desync info: {e}"); - } - - Log.Message($"Desync info writing took {watch.ElapsedMilliseconds}"); - } - - private string GetLocalTraces() - { - var traceMessage = ""; - var localCount = local.desyncStackTraceHashes.Count; - var remoteCount = remote.desyncStackTraceHashes.Count; - int count = Math.Min(localCount, remoteCount); - - if (diffAt == -1) - { - if (count == 0) - { - return $"No traces (remote: {remoteCount}, local: {localCount})"; - } - - traceMessage = "Note: trace hashes are equal between local and remote\n\n"; - } - else if (!diffAtFound) - { - traceMessage = "Note: traces differ in amount, but the existing ones are equal. This means that a tick" + - " has ended sooner on one of the connection sides\n\n"; - } - - traceMessage += local.GetFormattedStackTracesForRange(diffAt); - return traceMessage; - } - - private string GetDesyncDetails() - { - var desyncInfo = new StringBuilder(); - - desyncInfo - .AppendLine("###Tick Data###") - .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}") - .AppendLine($"Last Valid Tick - Local|||{coordinator.lastValidTick}") - .AppendLine($"Last Valid Tick - Arbiter|||{coordinator.arbiterWasPlayingOnLastValidTick}") - .AppendLine("\n###Version Data###") - .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") - .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") - .AppendLine("\n###Debug Options###") - .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") - .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") - .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") - .AppendLine("\n###Server Info###") - .AppendLine($"Player Count|||{Multiplayer.session.players.Count}") - .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") - .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") - .AppendLine($"Map Count|||{Find.Maps?.Count.ToStringSafe()}") - .AppendLine("\n###CPU Info###") - .AppendLine($"Processor Name|||{SystemInfo.processorType}") - .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}") - .AppendLine($"Thread Count|||{SystemInfo.processorCount}") - .AppendLine("\n###GPU Info###") +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using HarmonyLib; +using JetBrains.Annotations; +using Multiplayer.Common; +using Multiplayer.Common.Util; +using RimWorld; +using UnityEngine; +using Verse; +using CompressionLevel = System.IO.Compression.CompressionLevel; + +namespace Multiplayer.Client.Desyncs; + +public class SaveableDesyncInfo( + SyncCoordinator coordinator, + ClientSyncOpinion local, + ClientSyncOpinion remote, + int diffAt, + bool diffAtFound) +{ + public readonly ClientSyncOpinion local = local; + public readonly ClientSyncOpinion remote = remote; + public readonly int diffAt = diffAt; + private readonly Task metadata = Task.Run(MetadataGenerator.Generate); + private readonly Task replay = Task.Run(SaveReplayIfApplicable); + + public bool ReadyToSave => metadata.IsCompleted && replay.IsCompleted; + + public void Save([CanBeNull] HostInfo hostInfo) + { + var watch = Stopwatch.StartNew(); + + try + { + var desyncFilePath = FindFileNameForNextDesyncFile(); + using var zip = MpZipFile.Open(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip"), ZipArchiveMode.Create); + + zip.AddEntry("desync_info", GetDesyncDetails()); + + zip.AddEntry("local_traces.txt", GetLocalTraces()); + zip.AddEntry("host_traces.txt", hostInfo?.Traces ?? "No host traces"); + + zip.AddEntry("local_jitted_methods.txt", JittedMethods.GetJittedMethodsString()); + zip.AddEntry("host_jitted_methods.txt", hostInfo?.JittedMethods ?? "No host jitted methods"); + + var extraLogs = LogGenerator.PrepareLogData(); + if (extraLogs != null) zip.AddEntry("local_logs.txt", extraLogs); + + zip.AddEntry("local_metadata.txt", metadata.Result); + + try + { + replay.Wait(); + } + catch (AggregateException e) + { + if (e.InnerExceptions.SingleOrDefault(inner => inner is TaskCanceledException) == null) throw; + } + if (replay.IsCompletedSuccessfully) { + var replayFile = replay.Result; + zip.CreateEntryFromFile(replayFile.FullName, "replay.rwmts", CompressionLevel.NoCompression); + DeleteFileSilent(replayFile); + } + } + catch (Exception e) + { + Log.Error($"Exception writing desync info: {e}"); + } + + Log.Message($"Desync info writing took {watch.ElapsedMilliseconds}"); + } + + private string GetLocalTraces() + { + var traceMessage = ""; + var localCount = local.desyncStackTraceHashes.Count; + var remoteCount = remote.desyncStackTraceHashes.Count; + int count = Math.Min(localCount, remoteCount); + + if (diffAt == -1) + { + if (count == 0) + { + return $"No traces (remote: {remoteCount}, local: {localCount})"; + } + + traceMessage = "Note: trace hashes are equal between local and remote\n\n"; + } + else if (!diffAtFound) + { + traceMessage = "Note: traces differ in amount, but the existing ones are equal. This means that a tick" + + " has ended sooner on one of the connection sides\n\n"; + } + + traceMessage += local.GetFormattedStackTracesForRange(diffAt); + return traceMessage; + } + + private string GetDesyncDetails() + { + var desyncInfo = new StringBuilder(); + + desyncInfo + .AppendLine("###Tick Data###") + .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}") + .AppendLine($"Last Valid Tick - Local|||{coordinator.lastValidTick}") + .AppendLine($"Last Valid Tick - Arbiter|||{coordinator.arbiterWasPlayingOnLastValidTick}") + .AppendLine("\n###Version Data###") + .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") + .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") + .AppendLine("\n###Debug Options###") + .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") + .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") + .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") + .AppendLine("\n###Server Info###") + .AppendLine($"Player Count|||{Multiplayer.session.players.Count}") + .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") + .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") + .AppendLine($"Map Count|||{Find.Maps?.Count.ToStringSafe()}") + .AppendLine("\n###CPU Info###") + .AppendLine($"Processor Name|||{SystemInfo.processorType}") + .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}") + .AppendLine($"Thread Count|||{SystemInfo.processorCount}") + .AppendLine("\n###GPU Info###") +#if !RIMWORLD_1_7 .AppendLine($"GPU Family|||{SystemInfo.graphicsDeviceVendor}") - .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}") - .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}") - .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}") - .AppendLine("\n###RAM Info###") - .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}") - .AppendLine("\n###OS Info###") - .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") - .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}"); - - return desyncInfo.ToString(); - } - - private static string FindFileNameForNextDesyncFile() - { - const string FilePrefix = "Desync-"; - const string FileExtension = ".zip"; - - //Find all current existing desync zips - var files = new DirectoryInfo(Multiplayer.DesyncsDir).GetFiles($"{FilePrefix}*{FileExtension}"); - - const int MaxFiles = 10; - - //Delete any pushing us over the limit, and reserve room for one more - if (files.Length > MaxFiles - 1) - files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); - - //Find the current max desync number - int max = 0; - foreach (var f in files) - if (int.TryParse( - f.Name.Substring(FilePrefix.Length, f.Name.Length - FilePrefix.Length - FileExtension.Length), - out int result) && result > max) - max = result; - - return $"{FilePrefix}{max + 1:00}"; - } - - private static Task SaveReplayIfApplicable() - { - if (!Multiplayer.settings.includeReplayInDesync) return Task.FromCanceled(new CancellationToken(true)); - - try - { - var tmp = new FileInfo(Path.Combine(Multiplayer.DesyncsDir, "desync-replay.tmp.zip")); - if (tmp.Exists) tmp.Delete(); - Replay.ForSaving(tmp).WriteData(Multiplayer.session.dataSnapshot); - return Task.FromResult(tmp); - } - catch (Exception e) - { - Log.Error($"Failed to save replay for desync: {e}"); - return Task.FromException(e); - } - } - - private static void DeleteFileSilent(FileInfo file) - { - try - { - file.Delete(); - } - catch (IOException) - { - } - } - - public record HostInfo([CanBeNull] string Traces, [CanBeNull] string JittedMethods); -} +#endif + .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}") + .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}") + .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}") + .AppendLine("\n###RAM Info###") + .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}") + .AppendLine("\n###OS Info###") + .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") + .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}"); + + return desyncInfo.ToString(); + } + + private static string FindFileNameForNextDesyncFile() + { + const string FilePrefix = "Desync-"; + const string FileExtension = ".zip"; + + //Find all current existing desync zips + var files = new DirectoryInfo(Multiplayer.DesyncsDir).GetFiles($"{FilePrefix}*{FileExtension}"); + + const int MaxFiles = 10; + + //Delete any pushing us over the limit, and reserve room for one more + if (files.Length > MaxFiles - 1) + files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); + + //Find the current max desync number + int max = 0; + foreach (var f in files) + if (int.TryParse( + f.Name.Substring(FilePrefix.Length, f.Name.Length - FilePrefix.Length - FileExtension.Length), + out int result) && result > max) + max = result; + + return $"{FilePrefix}{max + 1:00}"; + } + + private static Task SaveReplayIfApplicable() + { + if (!Multiplayer.settings.includeReplayInDesync) return Task.FromCanceled(new CancellationToken(true)); + + try + { + var tmp = new FileInfo(Path.Combine(Multiplayer.DesyncsDir, "desync-replay.tmp.zip")); + if (tmp.Exists) tmp.Delete(); + Replay.ForSaving(tmp).WriteData(Multiplayer.session.dataSnapshot); + return Task.FromResult(tmp); + } + catch (Exception e) + { + Log.Error($"Failed to save replay for desync: {e}"); + return Task.FromException(e); + } + } + + private static void DeleteFileSilent(FileInfo file) + { + try + { + file.Delete(); + } + catch (IOException) + { + } + } + + public record HostInfo([CanBeNull] string Traces, [CanBeNull] string JittedMethods); +} diff --git a/Source/Client/Desyncs/StackTraceLogItem.cs b/Source/Client/Desyncs/StackTraceLogItem.cs index b7da27da5..a57d6c69d 100644 --- a/Source/Client/Desyncs/StackTraceLogItem.cs +++ b/Source/Client/Desyncs/StackTraceLogItem.cs @@ -1,92 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Multiplayer.Client.Desyncs; -using Verse; - -namespace Multiplayer.Client -{ - public abstract class StackTraceLogItem : IDisposable - { - public int tick; - public int hash; - - public abstract string AdditionalInfo { get; } - - public abstract string StackTraceString { get; } - - public override string ToString() - { - return $"Tick:{tick} Hash:{hash} '{AdditionalInfo}'\n{StackTraceString}"; - } - - public virtual void Dispose() - { - } - } - - public class StackTraceLogItemObj : StackTraceLogItem - { - public string info1; - public string info2; - - public override string AdditionalInfo => $"{info1} {info2}"; - - public override string StackTraceString => ""; - } - - public class StackTraceLogItemRaw : StackTraceLogItem - { - public const int MaxDepth = 32; - public long[] raw = new long[MaxDepth]; +using System; +using System.Collections.Generic; +using System.Text; +using Multiplayer.Client.Desyncs; +using Verse; + +namespace Multiplayer.Client +{ + public abstract class StackTraceLogItem : IDisposable + { + public int tick; + public int hash; + + public abstract string AdditionalInfo { get; } + + public abstract string StackTraceString { get; } + + public override string ToString() + { + return $"Tick:{tick} Hash:{hash} '{AdditionalInfo}'\n{StackTraceString}"; + } + + public virtual void Dispose() + { + } + } + + public class StackTraceLogItemObj : StackTraceLogItem + { + public string info1; + public string info2; + + public override string AdditionalInfo => $"{info1} {info2}"; + + public override string StackTraceString => ""; + } + + public class StackTraceLogItemRaw : StackTraceLogItem + { + public const int MaxDepth = 32; + public long[] raw = new long[MaxDepth]; public int depth; - - public int ticksGame; - public ulong rngState; - public ThingDef thingDef; - public int thingId; - public string factionName; - public string moreInfo; - - public override string AdditionalInfo => $"{ticksGame} {thingDef}{thingId} {factionName} {depth} {rngState} {moreInfo}"; - - private static Dictionary methodNameCache = new(); - - public override string StackTraceString - { + public string managedTrace; + + public int ticksGame; + public ulong rngState; + public ThingDef thingDef; + public int thingId; + public string factionName; + public string moreInfo; + + public override string AdditionalInfo => $"{ticksGame} {thingDef}{thingId} {factionName} {depth} {rngState} {moreInfo}"; + + private static Dictionary methodNameCache = new(); + + public override string StackTraceString + { get { + if (managedTrace != null) return managedTrace; var builder = new StringBuilder(); - for (int i = 0; i < depth; i++) - { - var addr = raw[i]; - - if (!methodNameCache.TryGetValue(addr, out string method)) - methodNameCache[addr] = method = Native.MethodNameFromAddr(raw[i], false); - - builder.AppendLine(method != null ? SyncCoordinator.MethodNameWithIL(method) : "Null"); - } - - return builder.ToString(); - } - } - - public static StackTraceLogItemRaw GetFromPool() - { - return SimplePool.Get(); - } - + for (int i = 0; i < depth; i++) + { + var addr = raw[i]; + + if (!methodNameCache.TryGetValue(addr, out string method)) + methodNameCache[addr] = method = Native.MethodNameFromAddr(raw[i], false); + + builder.AppendLine(method != null ? SyncCoordinator.MethodNameWithIL(method) : "Null"); + } + + return builder.ToString(); + } + } + + public static StackTraceLogItemRaw GetFromPool() + { + return SimplePool.Get(); + } + public override void Dispose() { + managedTrace = null; depth = 0; - ticksGame = 0; - rngState = 0; - thingId = 0; - thingDef = null; - factionName = null; - moreInfo = null; - - SimplePool.Return(this); - } - } -} + ticksGame = 0; + rngState = 0; + thingId = 0; + thingDef = null; + factionName = null; + moreInfo = null; + + SimplePool.Return(this); + } + } +} diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 9eea55518..51e3f143e 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,959 +1,959 @@ -using HarmonyLib; -using KTrie; -using Multiplayer.API; -using Multiplayer.Client.Factions; -using RimWorld; -using RimWorld.Planet; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.Remoting.Messaging; -using UnityEngine; -using Verse; -using Verse.AI; -using Verse.Sound; -using static HarmonyLib.AccessTools; - -namespace Multiplayer.Client.Patches; - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRow))] -public static class MainTabWindow_QuestsDoRowPatch -{ - public static void Prefix(ref Rect rect, Quest quest) - { - if (Multiplayer.Client != null && !Multiplayer.settings.hideOtherPlayersQuests) - { - Faction playerFaction; - if (quest.TryGetPlayerFaction(out playerFaction)) - { - Rect iconRect = new Rect(rect.x + 2f, rect.y + 2f, 4f, rect.height - 4f); - Widgets.DrawBoxSolid(iconRect, playerFaction.Color); - rect.xMin += 8f; - TooltipHandler.TipRegion(rect, "MpQuestDesc".Translate(playerFaction.Name, playerFaction == Faction.OfPlayer ? ". (you)" : ".")); - } - else - { - TooltipHandler.TipRegion(rect, "MpPublicQuest".Translate()); - } - } - } -} - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.ShouldListNow))] -public static class MainTabWindow_QuestsShouldListNowPatch -{ - static bool Prefix(Quest quest, ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.settings.hideOtherPlayersQuests) - { - Faction playerFaction; - if (quest.TryGetPlayerFaction(out playerFaction) && playerFaction != Faction.OfPlayer) - { - __result = false; - return false; - } - } - return true; - } -} - -[HarmonyPatch(typeof(WorldObject), nameof(WorldObject.ShowRelatedQuests), MethodType.Getter)] -static class WorldObjectShowRelatedQuestsPatch -{ - static void Postfix(ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result = false; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.ShowRelatedQuests), MethodType.Getter)] -static class SettlementShowRelatedQuestsPatch -{ - static void Postfix(ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result = false; - } -} - -[HarmonyPatch(typeof(WorldInspectPane), nameof(WorldInspectPane.PaneTopY), MethodType.Getter)] -static class WorldInspectPanePaneTopYPatch -{ - static void Postfix(ref float __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result += 35f; - } -} - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRewardsPrefsButton))] -public static class MainTabWindow_QuestsDoRewardsPrefsButtonPatch -{ - public static bool Prefix(MainTabWindow_Quests __instance, Rect rect, out Rect __result) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) - { - __result = rect; - return true; - } - rect.yMin = rect.yMax - 40f; - - float buttonHeight = 40f; - - float rewardButtonWidth = rect.width * 0.5f - 5f; - float toggleButtonWidth = rect.width - rewardButtonWidth - 10f; - - Rect rewardRect = new Rect(rect.x, rect.yMax - buttonHeight, rewardButtonWidth, buttonHeight); - Text.Font = GameFont.Small; - if (Widgets.ButtonText(rewardRect, "ChooseRewards".Translate(), true, true, true)) - { - Find.WindowStack.Add(new Dialog_RewardPrefsConfig()); - } - - Rect toggleRect = new Rect(rewardRect.xMax + 10f, rewardRect.y, toggleButtonWidth, buttonHeight); - bool value = Multiplayer.settings.hideOtherPlayersQuests; - string label = value ? "MpHideQuestsOn".Translate() : "MpHideQuestsOff".Translate(); - if (Widgets.ButtonText(toggleRect, label, true, true)) - { - Multiplayer.settings.hideOtherPlayersQuests = !value; - SoundDefOf.Tick_High.PlayOneShotOnCamera(); - } - TooltipHandler.TipRegion(toggleRect, "MpHideQuestsDesc".Translate()); - - __result = rect; - return false; - } -} - -[HarmonyPatch(typeof(ColonistBar), nameof(ColonistBar.CheckRecacheEntries))] -public static class ColonistBarCheckRecacheEntriesPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var findMapsGetter = AccessTools.PropertyGetter(typeof(Find), nameof(Find.Maps)); - foreach (var inst in insts) - { - if (inst.Calls(findMapsGetter)) - { - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(ColonistBarCheckRecacheEntriesPatch), nameof(GetFilteredMaps))); - } - else - { - yield return inst; - } - } - } - - public static IEnumerable GetFilteredMaps() - { - if (Multiplayer.Client == null || !Multiplayer.settings.hideOtherPlayersInColonistBar) - return Find.Maps; - - return Find.Maps.Where(map => - map.mapPawns.FreeColonistsSpawned.Any() || - map.Parent?.Faction == Faction.OfPlayer); - } -} - -[HarmonyPatch(typeof(WorldRoutePlanner), nameof(WorldRoutePlanner.DoRoutePlannerButton))] -public static class WorldRoutePlannerPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || Multiplayer.RealPlayerFaction != Multiplayer.WorldComp.spectatorFaction; - } -} - -[HarmonyPatch(typeof(WorldGizmoUtility), nameof(WorldGizmoUtility.TryGetCaravanGizmo))] -public static class WorldGizmoUtilityTryGetCaravanGizmoPatch -{ - static bool Prefix(ref Gizmo gizmo, ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - { - gizmo = null; - __result = false; - return false; - } - return true; - } -} - -[HarmonyPatch(typeof(MainButtonsRoot), nameof(MainButtonsRoot.DoButtons))] -static class MainButtonsRootDoButtonsPatch -{ - private static bool IsSpectator => - Multiplayer.Client != null && - Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction; - - private static readonly string[] SpectatorButtons = { "Menu", "World" }; - - private static readonly FieldRef> AllButtonsRef = AccessTools.FieldRefAccess>("allButtonsInOrder"); - - private static readonly Dictionary SpectatorButtonOrder = SpectatorButtons.Select((name, idx) => new { name, idx }).ToDictionary(x => x.name, x => x.idx); - - [HarmonyPrefix] - private static bool ReplaceOriginalDrawing(MainButtonsRoot __instance) - { - if (!IsSpectator) - return true; - - try - { - var allButtons = AllButtonsRef(__instance); - if (allButtons == null) - return true; - - var toDraw = SpectatorButtons - .Select(name => allButtons.Find(b => b.defName == name)) - .Where(b => b?.Worker.Visible ?? false) - .ToList(); - - DrawCornerButtons(toDraw); - return false; - } - catch (Exception ex) - { - Log.Error($"[MainButtonsRootPatch] ReplaceOriginalDrawing failed: {ex}"); - return true; - } - } - - [HarmonyPostfix] - private static void FilterButtons(MainButtonsRoot __instance) - { - try - { - var allButtons = AllButtonsRef(__instance); - if (allButtons == null) - return; - - if (IsSpectator) - { - // Keep only SpectatorButtons, sorted by our map - var filtered = allButtons - .Where(b => SpectatorButtonOrder.ContainsKey(b.defName)) - .OrderBy(b => SpectatorButtonOrder[b.defName]) - .ToList(); - - AllButtonsRef(__instance) = filtered; - } - else - { - // Restore original full list in normal mode - var original = DefDatabase.AllDefsListForReading - .Where(b => b.order >= 0) - .OrderBy(b => b.order) - .ToList(); - - AllButtonsRef(__instance) = original; - } - } - catch (Exception ex) - { - Log.Error($"[MainButtonsRootPatch] FilterButtons failed: {ex}"); - } - } - - private static void DrawCornerButtons(List buttons) - { - const float buttonHeight = 35f; - const float defaultPadding = 20f; - const float spacing = 0f; - - float x = UI.screenWidth; - float y = UI.screenHeight - buttonHeight; - - foreach (var btn in buttons) - { - float padding = btn.defName == "World" ? 120f : defaultPadding; - float width = btn.ShortenedLabelCapWidth + padding; - - x -= width; - btn.Worker.DoButton(new Rect(x, y, width, buttonHeight)); - x -= spacing; - } - } -} - - -[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] -static class DisableDraftGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) - { - return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); - } -} - -[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] -static class PawnChangeRelationGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) - { - foreach (var gizmo in gizmos) - yield return gizmo; - - if (Multiplayer.Client == null || Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - yield break; - - if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) - { - var otherFaction = __instance.Faction; - - yield return new Command_Action - { - defaultLabel = "Change faction relation", - icon = MultiplayerStatic.ChangeRelationIcon, - action = () => - { - List list = new List - { - new(FactionRelationKind.Hostile.ToString(), () => - { - SetRelation(otherFaction, FactionRelationKind.Hostile); - }), - new(FactionRelationKind.Neutral.ToString(), () => - { - SetRelation(otherFaction, FactionRelationKind.Neutral); - }) - }; - - Find.WindowStack.Add(new FloatMenu(list)); - } - }; - } - } - - [SyncMethod] - static void SetRelation(Faction other, FactionRelationKind kind) - { - Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); - - foreach (Map map in Find.Maps) - map.attackTargetsCache.Notify_FactionHostilityChanged(Faction.OfPlayer, other); - } -} - -[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] -static class CheckDefeatedPatch -{ - static bool Prefix(Settlement factionBase) - { - return factionBase.Faction is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] -static class CheckRemoveMapNowPatch -{ - static bool Prefix(MapParent __instance) - { - if (Multiplayer.Client == null) return true; - if (__instance is Camp) return true; - - bool parentBelongsToNonPlayerFaction = __instance.Faction is not { IsPlayer: true }; - - return parentBelongsToNonPlayerFaction; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] -static class SettlementAttackablePatch -{ - static bool Prefix(Settlement __instance) => __instance.Faction is not { IsPlayer: true }; -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] -static class SettlementNullFactionPatch1 -{ - static bool Prefix(Settlement __instance, ref Material __result) - { - if (__instance.factionInt == null) - { - __result = BaseContent.BadMat; - return false; - } - - return true; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] -static class SettlementNullFactionPatch2 -{ - static bool Prefix(Settlement __instance, ref Texture2D __result) - { - if (__instance.factionInt == null) - { - __result = BaseContent.BadTex; - return false; - } - - return true; - } -} - -[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetNaturalGoodwill))] -static class GetNaturalGoodwillPatch -{ - static bool Prefix(Faction other) - { - return other is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetMaxGoodwill))] -static class GetMaxGoodwillPatch -{ - static bool Prefix(Faction other) - { - return other is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(ScenPart_StartingAnimal), nameof(ScenPart_StartingAnimal.PetWeight))] -static class StartingAnimalPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var playerFactionField = AccessTools.Field(typeof(GameInitData), nameof(GameInitData.playerFaction)); - var factionOfPlayer = FactionOfPlayer; - - foreach (var inst in insts) - { - yield return inst; - - if (inst.operand as FieldInfo == playerFactionField) - yield return new CodeInstruction(OpCodes.Call, factionOfPlayer.Method); - } - } - - static Faction FactionOfPlayer(Faction f) - { - return Faction.OfPlayer; - } -} - -[HarmonyPatch(typeof(Pawn), nameof(Pawn.IsColonist), MethodType.Getter)] -static class PawnIsColonistPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isPlayerMethodGetter = AccessTools.PropertyGetter(typeof(Faction), nameof(Faction.IsPlayer)); - var factionIsPlayer = FactionIsPlayer; - - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isPlayerMethodGetter) - inst.operand = factionIsPlayer.Method; - - yield return inst; - } - } - - static bool FactionIsPlayer(Faction f) - { - return f == Find.FactionManager.OfPlayer; - } -} - -[HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] -static class RecacheColonistBelieverCountPatch -{ - private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive_FreeColonists_NoCryptosleep)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == allColonists) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(ColonistsAllPlayerFactions)); - yield return inst; - } - } - - private static List colonistsAllFactions = new(); - - private static List ColonistsAllPlayerFactions() - { - colonistsAllFactions.Clear(); - - foreach (var p in PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive) - { - if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) - colonistsAllFactions.Add(p); - } - - return colonistsAllFactions; - } - - // Note: IsColonist is patched above to compare to Faction.OfPlayer - public static bool IsColonistAnyFaction(Pawn p) - { - if (p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike) - return !p.IsSlave || p.guest.SlaveIsSecure; - return false; - } - - public static bool IsColonyMechAnyFaction(Pawn p) - { - if (ModsConfig.BiotechActive && p.RaceProps.IsMechanoid && p.Faction is { IsPlayer: true } && p.MentalStateDef == null) - return p.HostFaction == null || p.IsSlave; - return false; - } - - public static bool IsColonySubhumanAnyFaction(Pawn p) - { - return p.IsSubhuman && p.Faction is { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.AnyPawnBlockingMapRemoval), MethodType.Getter)] -static class AnyPawnBlockingMapRemovalPatch -{ - private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); - private static MethodInfo isColonyMech = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMech)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isColonyMech) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMechAnyFaction)); - - yield return inst; - } - } - - static void Postfix(MapPawns __instance, ref bool __result) - { - for (int i = 0; i < __instance.pawnsSpawned.Count; i++) - { - var p = __instance.pawnsSpawned[i]; - if (p.Faction is { IsPlayer: true } || p.HostFaction is { IsPlayer: true }) - { - Job curJob = p.CurJob; - if (curJob is { exitMapOnArrival: true }) - { - __result = true; - break; - } - - if (p.health.hediffSet.InLabor()) - { - __result = true; - break; - } - } - } - } -} - -[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.IsValidColonyPawn))] -static class IsValidColonyPawnPatch -{ - private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); - private static MethodInfo isColonySubhuman = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonySubhuman)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isColonist) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); - - if (inst.operand as MethodInfo == isColonySubhuman) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhumanAnyFaction)); - - yield return inst; - } - } -} - -[HarmonyPatch(typeof(Precept_Role), nameof(Precept_Role.ValidatePawn))] -static class ValidatePawnPatch -{ - private static MethodInfo isFreeNonSlaveColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeNonSlaveColonist)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isFreeNonSlaveColonist) - inst.operand = AccessTools.Method(typeof(ValidatePawnPatch), nameof(IsFreeNonSlaveColonistAnyFaction)); - - yield return inst; - } - } - - public static bool IsFreeNonSlaveColonistAnyFaction(Pawn p) - { - return p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike && p.HostFaction == null && !p.IsSlave; - } -} - -[HarmonyPatch(typeof(Faction), nameof(Faction.HasGoodwill), MethodType.Getter)] -static class PlayerFactionsHaveGoodwill -{ - static void Postfix(Faction __instance, ref bool __result) - { - if (__instance.IsPlayer) - __result = true; - } -} - -[HarmonyPatch(typeof(GenHostility), nameof(GenHostility.IsActiveThreatToPlayer))] -static class IsActiveThreatToAnyPlayer -{ - static void Postfix(IAttackTarget target, ref bool __result) - { - foreach (var f in Find.FactionManager.AllFactions) - if (f.IsPlayer) - __result |= GenHostility.IsActiveThreatTo(target, f); - } -} - -[HarmonyPatch] -static class TakeToBedGuestFactionPatch -{ - static IEnumerable TargetMethods() - { - yield return AccessTools.DeclaredMethod(typeof(JobDriver_TakeToBed), nameof(JobDriver_TakeToBed.CheckMakeTakeeGuest)); - yield return AccessTools.DeclaredMethod(typeof(JobDriver_CarryDownedPawn), nameof(JobDriver_CarryDownedPawn.CheckMakeTakeeGuest)); - } - static bool Prefix(JobDriver __instance) - { - var takee = __instance.job.GetTarget(TargetIndex.A).Pawn; - return takee?.Faction?.IsPlayer != true && takee?.HostFaction?.IsPlayer != true; - } -} - - -[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] -static class LetterStackReceiveOnlyMyFaction -{ - // todo the letter might get culled from the archive if it isn't in the stack and Sync depends on the archive - static void Postfix(LetterStack __instance, Letter let) - { - if (Multiplayer.RealPlayerFaction != Faction.OfPlayer) - __instance.letters.Remove(let); - } -} - -[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] -static class LetterStackReceiveSoundOnlyMyFaction -{ - private static MethodInfo PlayOneShotOnCamera = - typeof(SoundStarter).GetMethod(nameof(SoundStarter.PlayOneShotOnCamera)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == PlayOneShotOnCamera) - yield return new CodeInstruction( - OpCodes.Call, - SymbolExtensions.GetMethodInfo((SoundDef s, Map m) => PlaySoundReplacement(s, m))); - else - yield return inst; - } - } - - static void PlaySoundReplacement(SoundDef sound, Map map) - { - if (Multiplayer.RealPlayerFaction == Faction.OfPlayer) - sound.PlayOneShotOnCamera(map); - } -} - -[HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] -static class ApparelWornGraphicPathGetterPatch -{ - private static FieldInfo thingIDNumberField = AccessTools.Field(typeof(Thing), nameof(Thing.thingIDNumber)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - yield return inst; - - // This instruction is part of wornGraphicPaths[thingIDNumber % wornGraphicPaths.Count] - // The function makes sure the id is positive - if (inst.operand as FieldInfo == thingIDNumberField) - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(ApparelWornGraphicPathGetterPatch), nameof(MakeIdPositive))); - } - } - - private static int MakeIdPositive(int id) - { - return id < 0 ? -id : id; - } -} - -#region Map generation - -[HarmonyPatch(typeof(GenStep_Monolith), nameof(GenStep_Monolith.GenerateMonolith))] -public static class GenStepMonolithPatch -{ - static bool Prefix() - { - if (Multiplayer.Client == null) - return true; - - var anomalyComp = Current.Game.components.OfType().FirstOrDefault(); - - return anomalyComp?.monolith == null; - } -} - -[HarmonyPatch(typeof(ScenPart_ScatterThings), nameof(ScenPart_ScatterThings.GenerateIntoMap))] -static class ScenPartScatterThingsPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || FactionCreator.generatingMap; - } -} - -[HarmonyPatch(typeof(ScenPart_PlayerPawnsArriveMethod), nameof(ScenPart_PlayerPawnsArriveMethod.GenerateIntoMap))] -static class ScenPartPlayerPawnsArriveMethodPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || FactionCreator.generatingMap; - } -} - -#endregion - -[HarmonyPatch(typeof(CharacterCardUtility), nameof(CharacterCardUtility.DoTopStack))] -static class CharacterCardUtilityDontDrawIdeoPlate -{ - private static FieldInfo classicModeField = AccessTools.Field(typeof(IdeoManager), nameof(IdeoManager.classicMode)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - yield return inst; - - // Don't draw the ideo plate while choosing starting pawns in multifaction - if (inst.operand as FieldInfo == classicModeField) - { - yield return new CodeInstruction(OpCodes.Ldarg_2); - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(CharacterCardUtilityDontDrawIdeoPlate), nameof(DontDrawIdeoPlate))); - yield return new CodeInstruction(OpCodes.Or); - } - } - } - - private static bool DontDrawIdeoPlate(bool generating) - { - return Multiplayer.Client != null && generating; - } -} - -[HarmonyPatch(typeof(CompShuttle), nameof(CompShuttle.ContainedColonistCount), MethodType.Getter)] -static class CompShuttle_ContainedColonistCount_Patch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); - var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), - nameof(IsFreeColonistAnyPlayerFaction)); - - foreach (var ci in insts) - { - if (ci.Calls(isFreeColonist)) - { - ci.opcode = OpCodes.Call; - ci.operand = replacement; - } - yield return ci; - } - } - public static bool IsFreeColonistAnyPlayerFaction(Pawn pawn) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) - return pawn.IsFreeColonist; - - return pawn.Faction != null && - pawn.Faction.IsPlayer && - pawn.RaceProps.Humanlike && - (!pawn.IsSlave || pawn.guest.SlaveIsSecure) && - !pawn.IsSubhuman && - pawn.HostFaction == null; - } - -} - -[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.RemoveGravshipFromMap))] -static class RemoveGravshipFromMap_Patch -{ - static void Prefix(Building_GravEngine engine, out bool __state) - { - __state = false; - if (Multiplayer.Client == null) return; - if (!Multiplayer.GameComp.multifaction) return; - if (engine == null) return; - - engine.Map.PushFaction(engine.Faction); - - __state = true; - } - - static void Finalizer(Building_GravEngine engine, bool __state) - { - if (!__state) return; - engine.Map.PopFaction(); - } -} - -[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] -static class PlaceGravship_FactionContext_Patch -{ - static void Prefix(Gravship gravship, Map map, out bool __state) - { - __state = false; - if (Multiplayer.Client == null) return; - if (!Multiplayer.GameComp.multifaction) return; - - map.PushFaction(gravship.Faction); - - __state = true; - } - - static void Finalizer(Gravship gravship, Map map, bool __state) - { - if (!__state) return; - map.PopFaction(); - } -} - -[HarmonyPatch(typeof(QuestPart_LendColonistsToFaction), nameof(QuestPart_LendColonistsToFaction.Enable))] -static class QuestPart_LendColonistsToFaction_Enable_Patch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); - var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), - nameof(CompShuttle_ContainedColonistCount_Patch.IsFreeColonistAnyPlayerFaction)); - foreach (var ci in insts) - { - if (ci.Calls(isFreeColonist)) - { - ci.opcode = OpCodes.Call; - ci.operand = replacement; - } - yield return ci; - } - } -} - -[HarmonyPatch(typeof(Map), nameof(Map.IsPlayerHome), MethodType.Getter)] -static class Map_IsPlayerHome_Spectator_Patch -{ - static bool Prefix(Map __instance, ref bool __result) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction || - Faction.OfPlayer != Multiplayer.WorldComp.spectatorFaction) - return true; - - { - // Is repeat through all player faction a better idea? - if (!__instance.wasSpawnedViaGravShipLanding) - { - MapInfo mapInfo = __instance.info; - if (((mapInfo != null) ? mapInfo.parent : null) == null || __instance.info.parent.Faction?.IsPlayer != true || !__instance.info.parent.def.canBePlayerHome) - { - __result = GravshipUtility.PlayerHasGravEngine(__instance); - return false; - } - } - __result = true; - return false; - } - } -} - -[HarmonyPatch] -static class Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch -{ - static MethodInfo TargetMethod() - { - return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(Pawn_TraderTracker), nameof(Pawn_TraderTracker.ColonyThingsWillingToBuy))); - } - public static List AllBuildingsColonistOfDefOfPlayer(this ListerBuildings lister, ThingDef def) - { - if (def.CanHaveFaction) - return lister.AllBuildingsColonistOfDef(def).FindAll(building => building.Faction == Faction.OfPlayer); - else - return lister.AllBuildingsColonistOfDef(def); - } - public static IEnumerable AllColonistBuildingsOfTypeOfPlayer(this ListerBuildings lister) - { - return lister.AllColonistBuildingsOfType().Where(t => - { - if (t is Building building && (!building.def.CanHaveFaction || building.Faction == Faction.OfPlayer)) - return true; - return false; - }); - } - static IEnumerable Transpiler(IEnumerable insts) - { - var allBuildingsColonistOfDef = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef)); - var allBuildingsColonistOfDefOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllBuildingsColonistOfDefOfPlayer)); - var allColonistBuildingsOfType = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllColonistBuildingsOfType)).MakeGenericMethod([typeof(IHaulSource)]); - var allColonistBuildingsOfTypeOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllColonistBuildingsOfTypeOfPlayer)).MakeGenericMethod([typeof(IHaulSource)]); - foreach (var ci in insts) - { - if (ci.Calls(allBuildingsColonistOfDef)) - { - ci.opcode = OpCodes.Call; - ci.operand = allBuildingsColonistOfDefOfPlayer; - } - else if (ci.Calls(allColonistBuildingsOfType)) - { - ci.opcode = OpCodes.Call; - ci.operand = allColonistBuildingsOfTypeOfPlayer; - } - yield return ci; - } - } -} - -[HarmonyPatch] -static class TradeUtility_AllLaunchableThingsForTrade_Patch -{ - static MethodInfo TargetMethod() - { - return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(TradeUtility), nameof(TradeUtility.AllLaunchableThingsForTrade))); - } - - public static IEnumerable AllPoweredOfPlayer(Map map) - { - return Building_OrbitalTradeBeacon.AllPowered(map).Where(beacon => beacon.Faction == Faction.OfPlayer); - } - static IEnumerable Transpiler(IEnumerable insts) - { - var allPowered = AccessTools.Method(typeof(Building_OrbitalTradeBeacon), nameof(Building_OrbitalTradeBeacon.AllPowered)); - var allPoweredOfPlayer = AccessTools.Method(typeof(TradeUtility_AllLaunchableThingsForTrade_Patch), nameof(TradeUtility_AllLaunchableThingsForTrade_Patch.AllPoweredOfPlayer)); - foreach (var ci in insts) - { - if (ci.Calls(allPowered)) - { - ci.opcode = OpCodes.Call; - ci.operand = allPoweredOfPlayer; - } - - yield return ci; - } - } -} +using HarmonyLib; +using KTrie; +using Multiplayer.API; +using Multiplayer.Client.Factions; +using RimWorld; +using RimWorld.Planet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +using UnityEngine; +using Verse; +using Verse.AI; +using Verse.Sound; +using static HarmonyLib.AccessTools; + +namespace Multiplayer.Client.Patches; + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRow))] +public static class MainTabWindow_QuestsDoRowPatch +{ + public static void Prefix(ref Rect rect, Quest quest) + { + if (Multiplayer.Client != null && !Multiplayer.settings.hideOtherPlayersQuests) + { + Faction playerFaction; + if (quest.TryGetPlayerFaction(out playerFaction)) + { + Rect iconRect = new Rect(rect.x + 2f, rect.y + 2f, 4f, rect.height - 4f); + Widgets.DrawBoxSolid(iconRect, playerFaction.Color); + rect.xMin += 8f; + TooltipHandler.TipRegion(rect, "MpQuestDesc".Translate(playerFaction.Name, playerFaction == Faction.OfPlayer ? ". (you)" : ".")); + } + else + { + TooltipHandler.TipRegion(rect, "MpPublicQuest".Translate()); + } + } + } +} + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.ShouldListNow))] +public static class MainTabWindow_QuestsShouldListNowPatch +{ + static bool Prefix(Quest quest, ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.settings.hideOtherPlayersQuests) + { + Faction playerFaction; + if (quest.TryGetPlayerFaction(out playerFaction) && playerFaction != Faction.OfPlayer) + { + __result = false; + return false; + } + } + return true; + } +} + +[HarmonyPatch(typeof(WorldObject), nameof(WorldObject.ShowRelatedQuests), MethodType.Getter)] +static class WorldObjectShowRelatedQuestsPatch +{ + static void Postfix(ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result = false; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.ShowRelatedQuests), MethodType.Getter)] +static class SettlementShowRelatedQuestsPatch +{ + static void Postfix(ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result = false; + } +} + +[HarmonyPatch(typeof(WorldInspectPane), nameof(WorldInspectPane.PaneTopY), MethodType.Getter)] +static class WorldInspectPanePaneTopYPatch +{ + static void Postfix(ref float __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result += 35f; + } +} + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRewardsPrefsButton))] +public static class MainTabWindow_QuestsDoRewardsPrefsButtonPatch +{ + public static bool Prefix(MainTabWindow_Quests __instance, Rect rect, out Rect __result) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) + { + __result = rect; + return true; + } + rect.yMin = rect.yMax - 40f; + + float buttonHeight = 40f; + + float rewardButtonWidth = rect.width * 0.5f - 5f; + float toggleButtonWidth = rect.width - rewardButtonWidth - 10f; + + Rect rewardRect = new Rect(rect.x, rect.yMax - buttonHeight, rewardButtonWidth, buttonHeight); + Text.Font = GameFont.Small; + if (Widgets.ButtonText(rewardRect, "ChooseRewards".Translate(), true, true, true)) + { + Find.WindowStack.Add(new Dialog_RewardPrefsConfig()); + } + + Rect toggleRect = new Rect(rewardRect.xMax + 10f, rewardRect.y, toggleButtonWidth, buttonHeight); + bool value = Multiplayer.settings.hideOtherPlayersQuests; + string label = value ? "MpHideQuestsOn".Translate() : "MpHideQuestsOff".Translate(); + if (Widgets.ButtonText(toggleRect, label, true, true)) + { + Multiplayer.settings.hideOtherPlayersQuests = !value; + SoundDefOf.Tick_High.PlayOneShotOnCamera(); + } + TooltipHandler.TipRegion(toggleRect, "MpHideQuestsDesc".Translate()); + + __result = rect; + return false; + } +} + +[HarmonyPatch(typeof(ColonistBar), nameof(ColonistBar.CheckRecacheEntries))] +public static class ColonistBarCheckRecacheEntriesPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var findMapsGetter = AccessTools.PropertyGetter(typeof(Find), nameof(Find.Maps)); + foreach (var inst in insts) + { + if (inst.Calls(findMapsGetter)) + { + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(ColonistBarCheckRecacheEntriesPatch), nameof(GetFilteredMaps))); + } + else + { + yield return inst; + } + } + } + + public static IEnumerable GetFilteredMaps() + { + if (Multiplayer.Client == null || !Multiplayer.settings.hideOtherPlayersInColonistBar) + return Find.Maps; + + return Find.Maps.Where(map => + map.mapPawns.FreeColonistsSpawned.Any() || + map.Parent?.Faction == Faction.OfPlayer); + } +} + +[HarmonyPatch(typeof(WorldRoutePlanner), nameof(WorldRoutePlanner.DoRoutePlannerButton))] +public static class WorldRoutePlannerPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || Multiplayer.RealPlayerFaction != Multiplayer.WorldComp.spectatorFaction; + } +} + +[HarmonyPatch(typeof(WorldGizmoUtility), nameof(WorldGizmoUtility.TryGetCaravanGizmo))] +public static class WorldGizmoUtilityTryGetCaravanGizmoPatch +{ + static bool Prefix(ref Gizmo gizmo, ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + { + gizmo = null; + __result = false; + return false; + } + return true; + } +} + +[HarmonyPatch(typeof(MainButtonsRoot), nameof(MainButtonsRoot.DoButtons))] +static class MainButtonsRootDoButtonsPatch +{ + private static bool IsSpectator => + Multiplayer.Client != null && + Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction; + + private static readonly string[] SpectatorButtons = { "Menu", "World" }; + + private static readonly FieldRef> AllButtonsRef = AccessTools.FieldRefAccess>("allButtonsInOrder"); + + private static readonly Dictionary SpectatorButtonOrder = SpectatorButtons.Select((name, idx) => new { name, idx }).ToDictionary(x => x.name, x => x.idx); + + [HarmonyPrefix] + private static bool ReplaceOriginalDrawing(MainButtonsRoot __instance) + { + if (!IsSpectator) + return true; + + try + { + var allButtons = AllButtonsRef(__instance); + if (allButtons == null) + return true; + + var toDraw = SpectatorButtons + .Select(name => allButtons.Find(b => b.defName == name)) + .Where(b => b?.Worker.Visible ?? false) + .ToList(); + + DrawCornerButtons(toDraw); + return false; + } + catch (Exception ex) + { + Log.Error($"[MainButtonsRootPatch] ReplaceOriginalDrawing failed: {ex}"); + return true; + } + } + + [HarmonyPostfix] + private static void FilterButtons(MainButtonsRoot __instance) + { + try + { + var allButtons = AllButtonsRef(__instance); + if (allButtons == null) + return; + + if (IsSpectator) + { + // Keep only SpectatorButtons, sorted by our map + var filtered = allButtons + .Where(b => SpectatorButtonOrder.ContainsKey(b.defName)) + .OrderBy(b => SpectatorButtonOrder[b.defName]) + .ToList(); + + AllButtonsRef(__instance) = filtered; + } + else + { + // Restore original full list in normal mode + var original = DefDatabase.AllDefsListForReading + .Where(b => b.order >= 0) + .OrderBy(b => b.order) + .ToList(); + + AllButtonsRef(__instance) = original; + } + } + catch (Exception ex) + { + Log.Error($"[MainButtonsRootPatch] FilterButtons failed: {ex}"); + } + } + + private static void DrawCornerButtons(List buttons) + { + const float buttonHeight = 35f; + const float defaultPadding = 20f; + const float spacing = 0f; + + float x = UI.screenWidth; + float y = UI.screenHeight - buttonHeight; + + foreach (var btn in buttons) + { + float padding = btn.defName == "World" ? 120f : defaultPadding; + float width = btn.ShortenedLabelCapWidth + padding; + + x -= width; + btn.Worker.DoButton(new Rect(x, y, width, buttonHeight)); + x -= spacing; + } + } +} + + +[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] +static class DisableDraftGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) + { + return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] +static class PawnChangeRelationGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) + { + foreach (var gizmo in gizmos) + yield return gizmo; + + if (Multiplayer.Client == null || Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + yield break; + + if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) + { + var otherFaction = __instance.Faction; + + yield return new Command_Action + { + defaultLabel = "Change faction relation", + icon = MultiplayerStatic.ChangeRelationIcon, + action = () => + { + List list = new List + { + new(FactionRelationKind.Hostile.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Hostile); + }), + new(FactionRelationKind.Neutral.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Neutral); + }) + }; + + Find.WindowStack.Add(new FloatMenu(list)); + } + }; + } + } + + [SyncMethod] + static void SetRelation(Faction other, FactionRelationKind kind) + { + Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); + + foreach (Map map in Find.Maps) + map.attackTargetsCache.Notify_FactionHostilityChanged(Faction.OfPlayer, other); + } +} + +[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] +static class CheckDefeatedPatch +{ + static bool Prefix(Settlement factionBase) + { + return factionBase.Faction is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] +static class CheckRemoveMapNowPatch +{ + static bool Prefix(MapParent __instance) + { + if (Multiplayer.Client == null) return true; + if (__instance is Camp) return true; + + bool parentBelongsToNonPlayerFaction = __instance.Faction is not { IsPlayer: true }; + + return parentBelongsToNonPlayerFaction; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] +static class SettlementAttackablePatch +{ + static bool Prefix(Settlement __instance) => __instance.Faction is not { IsPlayer: true }; +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] +static class SettlementNullFactionPatch1 +{ + static bool Prefix(Settlement __instance, ref Material __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadMat; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] +static class SettlementNullFactionPatch2 +{ + static bool Prefix(Settlement __instance, ref Texture2D __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadTex; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetNaturalGoodwill))] +static class GetNaturalGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetMaxGoodwill))] +static class GetMaxGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(ScenPart_StartingAnimal), nameof(ScenPart_StartingAnimal.PetWeight))] +static class StartingAnimalPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var playerFactionField = AccessTools.Field(typeof(GameInitData), nameof(GameInitData.playerFaction)); + var factionOfPlayer = FactionOfPlayer; + + foreach (var inst in insts) + { + yield return inst; + + if (inst.operand as FieldInfo == playerFactionField) + yield return new CodeInstruction(OpCodes.Call, factionOfPlayer.Method); + } + } + + static Faction FactionOfPlayer(Faction f) + { + return Faction.OfPlayer; + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.IsColonist), MethodType.Getter)] +static class PawnIsColonistPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isPlayerMethodGetter = AccessTools.PropertyGetter(typeof(Faction), nameof(Faction.IsPlayer)); + var factionIsPlayer = FactionIsPlayer; + + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isPlayerMethodGetter) + inst.operand = factionIsPlayer.Method; + + yield return inst; + } + } + + static bool FactionIsPlayer(Faction f) + { + return f == Find.FactionManager.OfPlayer; + } +} + +[HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] +static class RecacheColonistBelieverCountPatch +{ + private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive_FreeColonists_NoCryptosleep)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == allColonists) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(ColonistsAllPlayerFactions)); + yield return inst; + } + } + + private static List colonistsAllFactions = new(); + + private static List ColonistsAllPlayerFactions() + { + colonistsAllFactions.Clear(); + + foreach (var p in PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive) + { + if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) + colonistsAllFactions.Add(p); + } + + return colonistsAllFactions; + } + + // Note: IsColonist is patched above to compare to Faction.OfPlayer + public static bool IsColonistAnyFaction(Pawn p) + { + if (p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike) + return !p.IsSlave || p.guest.SlaveIsSecure; + return false; + } + + public static bool IsColonyMechAnyFaction(Pawn p) + { + if (ModsConfig.BiotechActive && p.RaceProps.IsMechanoid && p.Faction is { IsPlayer: true } && p.MentalStateDef == null) + return p.HostFaction == null || p.IsSlave; + return false; + } + + public static bool IsColonySubhumanAnyFaction(Pawn p) + { + return p.IsSubhuman && p.Faction is { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.AnyPawnBlockingMapRemoval), MethodType.Getter)] +static class AnyPawnBlockingMapRemovalPatch +{ + private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); + private static MethodInfo isColonyMech = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMech)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isColonyMech) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMechAnyFaction)); + + yield return inst; + } + } + + static void Postfix(MapPawns __instance, ref bool __result) + { + for (int i = 0; i < __instance.pawnsSpawned.Count; i++) + { + var p = __instance.pawnsSpawned[i]; + if (p.Faction is { IsPlayer: true } || p.HostFaction is { IsPlayer: true }) + { + Job curJob = p.CurJob; + if (curJob is { exitMapOnArrival: true }) + { + __result = true; + break; + } + + if (p.health.hediffSet.InLabor()) + { + __result = true; + break; + } + } + } + } +} + +[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.IsValidColonyPawn))] +static class IsValidColonyPawnPatch +{ + private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); + private static MethodInfo isColonySubhuman = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonySubhuman)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isColonist) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); + + if (inst.operand as MethodInfo == isColonySubhuman) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhumanAnyFaction)); + + yield return inst; + } + } +} + +[HarmonyPatch(typeof(Precept_Role), nameof(Precept_Role.ValidatePawn))] +static class ValidatePawnPatch +{ + private static MethodInfo isFreeNonSlaveColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeNonSlaveColonist)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isFreeNonSlaveColonist) + inst.operand = AccessTools.Method(typeof(ValidatePawnPatch), nameof(IsFreeNonSlaveColonistAnyFaction)); + + yield return inst; + } + } + + public static bool IsFreeNonSlaveColonistAnyFaction(Pawn p) + { + return p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike && p.HostFaction == null && !p.IsSlave; + } +} + +[HarmonyPatch(typeof(Faction), nameof(Faction.HasGoodwill), MethodType.Getter)] +static class PlayerFactionsHaveGoodwill +{ + static void Postfix(Faction __instance, ref bool __result) + { + if (__instance.IsPlayer) + __result = true; + } +} + +[HarmonyPatch(typeof(GenHostility), nameof(GenHostility.IsActiveThreatToPlayer))] +static class IsActiveThreatToAnyPlayer +{ + static void Postfix(IAttackTarget target, ref bool __result) + { + foreach (var f in Find.FactionManager.AllFactions) + if (f.IsPlayer) + __result |= GenHostility.IsActiveThreatTo(target, f); + } +} + +[HarmonyPatch] +static class TakeToBedGuestFactionPatch +{ + static IEnumerable TargetMethods() + { + yield return AccessTools.DeclaredMethod(typeof(JobDriver_TakeToBed), nameof(JobDriver_TakeToBed.CheckMakeTakeeGuest)); + yield return AccessTools.DeclaredMethod(typeof(JobDriver_CarryDownedPawn), nameof(JobDriver_CarryDownedPawn.CheckMakeTakeeGuest)); + } + static bool Prefix(JobDriver __instance) + { + var takee = __instance.job.GetTarget(TargetIndex.A).Pawn; + return takee?.Faction?.IsPlayer != true && takee?.HostFaction?.IsPlayer != true; + } +} + + +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] +static class LetterStackReceiveOnlyMyFaction +{ + // todo the letter might get culled from the archive if it isn't in the stack and Sync depends on the archive + static void Postfix(LetterStack __instance, Letter let) + { + if (Multiplayer.RealPlayerFaction != Faction.OfPlayer) + __instance.letters.Remove(let); + } +} + +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] +static class LetterStackReceiveSoundOnlyMyFaction +{ + private static MethodInfo PlayOneShotOnCamera = + typeof(SoundStarter).GetMethod(nameof(SoundStarter.PlayOneShotOnCamera)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == PlayOneShotOnCamera) + yield return new CodeInstruction( + OpCodes.Call, + SymbolExtensions.GetMethodInfo((SoundDef s, Map m) => PlaySoundReplacement(s, m))); + else + yield return inst; + } + } + + static void PlaySoundReplacement(SoundDef sound, Map map) + { + if (Multiplayer.RealPlayerFaction == Faction.OfPlayer) + sound.PlayOneShotOnCamera(map); + } +} + +[HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] +static class ApparelWornGraphicPathGetterPatch +{ + private static FieldInfo thingIDNumberField = AccessTools.Field(typeof(Thing), nameof(Thing.thingIDNumber)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + yield return inst; + + // This instruction is part of wornGraphicPaths[thingIDNumber % wornGraphicPaths.Count] + // The function makes sure the id is positive + if (inst.operand as FieldInfo == thingIDNumberField) + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(ApparelWornGraphicPathGetterPatch), nameof(MakeIdPositive))); + } + } + + private static int MakeIdPositive(int id) + { + return id < 0 ? -id : id; + } +} + +#region Map generation + +[HarmonyPatch(typeof(GenStep_Monolith), nameof(GenStep_Monolith.GenerateMonolith))] +public static class GenStepMonolithPatch +{ + static bool Prefix() + { + if (Multiplayer.Client == null) + return true; + + var anomalyComp = Current.Game.components.OfType().FirstOrDefault(); + + return anomalyComp?.monolith == null; + } +} + +[HarmonyPatch(typeof(ScenPart_ScatterThings), nameof(ScenPart_ScatterThings.GenerateIntoMap))] +static class ScenPartScatterThingsPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || FactionCreator.generatingMap; + } +} + +[HarmonyPatch(typeof(ScenPart_PlayerPawnsArriveMethod), nameof(ScenPart_PlayerPawnsArriveMethod.GenerateIntoMap))] +static class ScenPartPlayerPawnsArriveMethodPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || FactionCreator.generatingMap; + } +} + +#endregion + +[HarmonyPatch(typeof(CharacterCardUtility), nameof(CharacterCardUtility.DoTopStack))] +static class CharacterCardUtilityDontDrawIdeoPlate +{ + private static FieldInfo classicModeField = AccessTools.Field(typeof(IdeoManager), nameof(IdeoManager.classicMode)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + yield return inst; + + // Don't draw the ideo plate while choosing starting pawns in multifaction + if (inst.operand as FieldInfo == classicModeField) + { + yield return new CodeInstruction(OpCodes.Ldarg_2); + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(CharacterCardUtilityDontDrawIdeoPlate), nameof(DontDrawIdeoPlate))); + yield return new CodeInstruction(OpCodes.Or); + } + } + } + + private static bool DontDrawIdeoPlate(bool generating) + { + return Multiplayer.Client != null && generating; + } +} + +[HarmonyPatch(typeof(CompShuttle), nameof(CompShuttle.ContainedColonistCount), MethodType.Getter)] +static class CompShuttle_ContainedColonistCount_Patch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); + var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), + nameof(IsFreeColonistAnyPlayerFaction)); + + foreach (var ci in insts) + { + if (ci.Calls(isFreeColonist)) + { + ci.opcode = OpCodes.Call; + ci.operand = replacement; + } + yield return ci; + } + } + public static bool IsFreeColonistAnyPlayerFaction(Pawn pawn) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) + return pawn.IsFreeColonist; + + return pawn.Faction != null && + pawn.Faction.IsPlayer && + pawn.RaceProps.Humanlike && + (!pawn.IsSlave || pawn.guest.SlaveIsSecure) && + !pawn.IsSubhuman && + pawn.HostFaction == null; + } + +} + +[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.RemoveGravshipFromMap))] +static class RemoveGravshipFromMap_Patch +{ + static void Prefix(Building_GravEngine engine, out bool __state) + { + __state = false; + if (Multiplayer.Client == null) return; + if (!Multiplayer.GameComp.multifaction) return; + if (engine == null) return; + + engine.Map.PushFaction(engine.Faction); + + __state = true; + } + + static void Finalizer(Building_GravEngine engine, bool __state) + { + if (!__state) return; + engine.Map.PopFaction(); + } +} + +[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] +static class PlaceGravship_FactionContext_Patch +{ + static void Prefix(Gravship gravship, Map map, out bool __state) + { + __state = false; + if (Multiplayer.Client == null) return; + if (!Multiplayer.GameComp.multifaction) return; + + map.PushFaction(gravship.Faction); + + __state = true; + } + + static void Finalizer(Gravship gravship, Map map, bool __state) + { + if (!__state) return; + map.PopFaction(); + } +} + +[HarmonyPatch(typeof(QuestPart_LendColonistsToFaction), nameof(QuestPart_LendColonistsToFaction.Enable))] +static class QuestPart_LendColonistsToFaction_Enable_Patch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); + var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), + nameof(CompShuttle_ContainedColonistCount_Patch.IsFreeColonistAnyPlayerFaction)); + foreach (var ci in insts) + { + if (ci.Calls(isFreeColonist)) + { + ci.opcode = OpCodes.Call; + ci.operand = replacement; + } + yield return ci; + } + } +} + +[HarmonyPatch(typeof(Map), nameof(Map.IsPlayerHome), MethodType.Getter)] +static class Map_IsPlayerHome_Spectator_Patch +{ + static bool Prefix(Map __instance, ref bool __result) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction || + Faction.OfPlayer != Multiplayer.WorldComp.spectatorFaction) + return true; + + { + // Is repeat through all player faction a better idea? + if (!__instance.wasSpawnedViaGravShipLanding) + { + MapInfo mapInfo = __instance.info; + if (((mapInfo != null) ? mapInfo.parent : null) == null || __instance.info.parent.Faction?.IsPlayer != true || !__instance.info.parent.def.canBePlayerHome) + { + __result = GravshipUtility.PlayerHasGravEngine(__instance); + return false; + } + } + __result = true; + return false; + } + } +} + +[HarmonyPatch] +static class Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch +{ + static MethodInfo TargetMethod() + { + return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(Pawn_TraderTracker), nameof(Pawn_TraderTracker.ColonyThingsWillingToBuy))); + } + public static List AllBuildingsColonistOfDefOfPlayer(this ListerBuildings lister, ThingDef def) + { + if (def.CanHaveFaction) + return lister.AllBuildingsColonistOfDef(def).FindAll(building => building.Faction == Faction.OfPlayer); + else + return lister.AllBuildingsColonistOfDef(def); + } + public static IEnumerable AllColonistBuildingsOfTypeOfPlayer(this ListerBuildings lister) + { + return lister.AllColonistBuildingsOfType().Where(t => + { + if (t is Building building && (!building.def.CanHaveFaction || building.Faction == Faction.OfPlayer)) + return true; + return false; + }); + } + static IEnumerable Transpiler(IEnumerable insts) + { + var allBuildingsColonistOfDef = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef)); + var allBuildingsColonistOfDefOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllBuildingsColonistOfDefOfPlayer)); + var allColonistBuildingsOfType = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllColonistBuildingsOfType)).MakeGenericMethod([typeof(IHaulSource)]); + var allColonistBuildingsOfTypeOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllColonistBuildingsOfTypeOfPlayer)).MakeGenericMethod([typeof(IHaulSource)]); + foreach (var ci in insts) + { + if (ci.Calls(allBuildingsColonistOfDef)) + { + ci.opcode = OpCodes.Call; + ci.operand = allBuildingsColonistOfDefOfPlayer; + } + else if (ci.Calls(allColonistBuildingsOfType)) + { + ci.opcode = OpCodes.Call; + ci.operand = allColonistBuildingsOfTypeOfPlayer; + } + yield return ci; + } + } +} + +[HarmonyPatch] +static class TradeUtility_AllLaunchableThingsForTrade_Patch +{ + static MethodInfo TargetMethod() + { + return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(TradeUtility), nameof(TradeUtility.AllLaunchableThingsForTrade))); + } + + public static IEnumerable AllPoweredOfPlayer(Map map) + { + return Building_OrbitalTradeBeacon.AllPowered(map).Where(beacon => beacon.Faction == Faction.OfPlayer); + } + static IEnumerable Transpiler(IEnumerable insts) + { + var allPowered = AccessTools.Method(typeof(Building_OrbitalTradeBeacon), nameof(Building_OrbitalTradeBeacon.AllPowered)); + var allPoweredOfPlayer = AccessTools.Method(typeof(TradeUtility_AllLaunchableThingsForTrade_Patch), nameof(TradeUtility_AllLaunchableThingsForTrade_Patch.AllPoweredOfPlayer)); + foreach (var ci in insts) + { + if (ci.Calls(allPowered)) + { + ci.opcode = OpCodes.Call; + ci.operand = allPoweredOfPlayer; + } + + yield return ci; + } + } +} diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 5ea4b1760..d770b9c36 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -1,257 +1,258 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using HarmonyLib; -using Multiplayer.Client.AsyncTime; -using Multiplayer.Client.Comp; -using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Patches; -using Multiplayer.Client.Util; -using Multiplayer.Common; -using RimWorld; -using UnityEngine; -using Verse; -#if EDIT_COMPILE_RELOAD -using HotSwap; -#endif - -namespace Multiplayer.Client -{ - public static class Multiplayer - { - public const string WebsiteLink = "https://rimworldmultiplayer.com"; - // There is also a link in About.xml. Remember to update both - public const string DiscordLink = "https://discord.gg/S4bxXpv"; - - public static Harmony harmony = new("multiplayer"); - public static MpSettings settings; - - public static MultiplayerGame game; - public static MultiplayerSession session; - - public static Common.SyncSerialization serialization; - - public static MultiplayerServer LocalServer { get; set; } - public static Thread localServerThread; - - public static ConnectionBase Client => session?.client; - public static PacketLogWindow WriterLog => session?.writerLog; - public static PacketLogWindow ReaderLog => session?.readerLog; - public static bool IsReplay => session?.replay ?? false; - - public static string username; - - public static bool reloading; - - public static MultiplayerGameComp GameComp => game.gameComp; - public static MultiplayerWorldComp WorldComp => game.worldComp; - public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; - public static ThingsById ThingsById => game.thingsById; - - public static bool ShowDevInfo => Prefs.DevMode && settings.showDevInfo; - public static bool GhostMode => session is { ghostModeCheckbox: true }; - - public static Faction RealPlayerFaction => Client != null ? game.RealPlayerFaction : Faction.OfPlayer; - - public static bool ExecutingCmds => TickPatch.currentExecutingCmdType != null; - public static bool ExecutingCmdDebugTool => TickPatch.currentExecutingCmdType == CommandType.DebugTools; - public static bool Ticking => AsyncWorldTimeComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; - public static Map MapContext => AsyncTimeComp.tickingMap ?? AsyncTimeComp.executingCmdMap; - - public static bool dontSync; - public static bool ShouldSync => InInterface && !dontSync; - public static bool InInterface => - Client != null - && !Ticking - && !ExecutingCmds - && !reloading - && Current.ProgramState == ProgramState.Playing - && LongEventHandler.currentEvent == null; - - public static string ReplaysDir => GenFilePaths.FolderUnderSaveData("MpReplays"); - public static string DesyncsDir => GenFilePaths.FolderUnderSaveData("MpDesyncs"); - public static string LogsDir => GenFilePaths.FolderUnderSaveData("MpLogs"); - public static string CacheDir => GenFilePaths.FolderUnderSaveData("MpCache"); - - public static Stopwatch clock = Stopwatch.StartNew(); - - public static bool arbiterInstance; - public static bool loadingErrors; - public static Stopwatch harmonyWatch = new(); - - public static ModContentPack modContentPack; - - public static void InitMultiplayer(ModContentPack content) - { -#if EDIT_COMPILE_RELOAD - EcrLog.messageCallback = Log.Message; - EcrLog.errorCallback = Log.Error; - // EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "MultiplayerCommon.dll_orig"); - EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "Multiplayer.dll_orig"); -#endif - - modContentPack = content; - Native.EarlyInit( - Application.platform switch - { - RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, - RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, - RuntimePlatform.OSXEditor => Native.NativeOS.OSX, - RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, - _ => Native.NativeOS.Windows - }); - - DisableOmitFramePointer(); - JittedMethods.Init(); - - MultiplayerLoader.Multiplayer.settingsWindowDrawer = - rect => MpSettingsUI.DoSettingsWindowContents(settings, rect); - - using (DeepProfilerWrapper.Section("Multiplayer CacheTypeHierarchy")) - TypeCache.CacheTypeHierarchy(); - - using (DeepProfilerWrapper.Section("Multiplayer CacheTypeByName")) - TypeCache.CacheTypeByName(); - - if (GenCommandLine.CommandLineArgPassed("profiler")) - { - SimpleProfiler.CheckAvailable(); - Log.Message($"Profiler: {SimpleProfiler.available}"); - SimpleProfiler.Init("prof"); - } - - if (GenCommandLine.CommandLineArgPassed("arbiter")) - { - ArbiterWindowFix.Run(); - arbiterInstance = true; - } - - ScribeLike.provider = new ScribeProvider(); - settings = MultiplayerLoader.Multiplayer.instance!.GetSettings(); - - EarlyInit.ProcessEnvironment(); - EarlyInit.EarlyPatches(harmony); - MpReflection.allAssembliesHook = RwAllAssemblies; - EarlyInit.InitSync(); - CheckInterfaceVersions(); - - LongEventHandler.ExecuteWhenFinished(() => { - // Double Execute ensures it'll run last. - LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); - }); - - MultiplayerData.modCtorRoundMode = RoundMode.GetCurrentRoundMode(); - VersionChecker.Init(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using HarmonyLib; +using Multiplayer.Client.AsyncTime; +using Multiplayer.Client.Comp; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Patches; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; +#if EDIT_COMPILE_RELOAD +using HotSwap; +#endif + +namespace Multiplayer.Client +{ + public static class Multiplayer + { + public const string WebsiteLink = "https://rimworldmultiplayer.com"; + // There is also a link in About.xml. Remember to update both + public const string DiscordLink = "https://discord.gg/S4bxXpv"; + + public static Harmony harmony = new("multiplayer"); + public static MpSettings settings; + + public static MultiplayerGame game; + public static MultiplayerSession session; + + public static Common.SyncSerialization serialization; + + public static MultiplayerServer LocalServer { get; set; } + public static Thread localServerThread; + + public static ConnectionBase Client => session?.client; + public static PacketLogWindow WriterLog => session?.writerLog; + public static PacketLogWindow ReaderLog => session?.readerLog; + public static bool IsReplay => session?.replay ?? false; + + public static string username; + + public static bool reloading; + + public static MultiplayerGameComp GameComp => game.gameComp; + public static MultiplayerWorldComp WorldComp => game.worldComp; + public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; + public static ThingsById ThingsById => game.thingsById; + + public static bool ShowDevInfo => Prefs.DevMode && settings.showDevInfo; + public static bool GhostMode => session is { ghostModeCheckbox: true }; + + public static Faction RealPlayerFaction => Client != null ? game.RealPlayerFaction : Faction.OfPlayer; + + public static bool ExecutingCmds => TickPatch.currentExecutingCmdType != null; + public static bool ExecutingCmdDebugTool => TickPatch.currentExecutingCmdType == CommandType.DebugTools; + public static bool Ticking => AsyncWorldTimeComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; + public static Map MapContext => AsyncTimeComp.tickingMap ?? AsyncTimeComp.executingCmdMap; + + public static bool dontSync; + public static bool ShouldSync => InInterface && !dontSync; + public static bool InInterface => + Client != null + && !Ticking + && !ExecutingCmds + && !reloading + && Current.ProgramState == ProgramState.Playing + && LongEventHandler.currentEvent == null; + + public static string ReplaysDir => GenFilePaths.FolderUnderSaveData("MpReplays"); + public static string DesyncsDir => GenFilePaths.FolderUnderSaveData("MpDesyncs"); + public static string LogsDir => GenFilePaths.FolderUnderSaveData("MpLogs"); + public static string CacheDir => GenFilePaths.FolderUnderSaveData("MpCache"); + + public static Stopwatch clock = Stopwatch.StartNew(); + + public static bool arbiterInstance; + public static bool loadingErrors; + public static Stopwatch harmonyWatch = new(); + + public static ModContentPack modContentPack; + + public static void InitMultiplayer(ModContentPack content) + { +#if EDIT_COMPILE_RELOAD + EcrLog.messageCallback = Log.Message; + EcrLog.errorCallback = Log.Error; + // EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "MultiplayerCommon.dll_orig"); + EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "Multiplayer.dll_orig"); +#endif + + modContentPack = content; + Native.EarlyInit( + Application.platform switch + { + RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, + RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, + RuntimePlatform.OSXEditor => Native.NativeOS.OSX, + RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, + _ => Native.NativeOS.Windows + }); + + DisableOmitFramePointer(); + JittedMethods.Init(); + + MultiplayerLoader.Multiplayer.settingsWindowDrawer = + rect => MpSettingsUI.DoSettingsWindowContents(settings, rect); + + using (DeepProfilerWrapper.Section("Multiplayer CacheTypeHierarchy")) + TypeCache.CacheTypeHierarchy(); + + using (DeepProfilerWrapper.Section("Multiplayer CacheTypeByName")) + TypeCache.CacheTypeByName(); + + if (GenCommandLine.CommandLineArgPassed("profiler")) + { + SimpleProfiler.CheckAvailable(); + Log.Message($"Profiler: {SimpleProfiler.available}"); + SimpleProfiler.Init("prof"); + } + + if (GenCommandLine.CommandLineArgPassed("arbiter")) + { + ArbiterWindowFix.Run(); + arbiterInstance = true; + } + + ScribeLike.provider = new ScribeProvider(); + settings = MultiplayerLoader.Multiplayer.instance!.GetSettings(); + + EarlyInit.ProcessEnvironment(); + EarlyInit.EarlyPatches(harmony); + MpReflection.allAssembliesHook = RwAllAssemblies; + EarlyInit.InitSync(); + CheckInterfaceVersions(); + + LongEventHandler.ExecuteWhenFinished(() => { + // Double Execute ensures it'll run last. + LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); + }); + + MultiplayerData.modCtorRoundMode = RoundMode.GetCurrentRoundMode(); + VersionChecker.Init(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void DisableOmitFramePointer() { + if (!Native.IsMonoRuntime) return; Native.mini_parse_debug_option("disable_omit_fp"); - } - - private static void CheckInterfaceVersions() - { - var mpAssembly = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Multiplayer"); - var curVersion = new Version( - (mpAssembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0] as AssemblyFileVersionAttribute).Version - ); - - Log.Message($"Current MultiplayerAPI version: {curVersion}"); - - foreach (var mod in LoadedModManager.RunningMods) { - if (mod.assemblies.loadedAssemblies.NullOrEmpty()) - continue; - - if (mod == modContentPack) - continue; - - // Test if mod is using multiplayer api - if (!mod.assemblies.loadedAssemblies.Any(a => a.GetName().Name == MpVersion.ApiAssemblyName)) { - continue; - } - - // Retrieve the original dll - var info = MultiplayerData.GetModAssemblies(mod) - .Select(f => FileVersionInfo.GetVersionInfo(f.FullName)) - .FirstOrDefault(v => v.ProductName == "Multiplayer"); - - if (info == null) { - // There are certain mods that don't include the API, namely compat - // Can we test them? - continue; - } - - var version = new Version(info.FileVersion); - - Log.Message($"Mod {mod.Name} has MultiplayerAPI client ({version})"); - - if (curVersion > version) - Log.Warning($"Mod {mod.Name} uses an older API version (mod: {version}, current: {curVersion})"); - else if (curVersion < version) - Log.Error($"Mod {mod.Name} uses a newer API version! (mod: {version}, current: {curVersion})\nMake sure the Multiplayer mod is up to date"); - } - } - - public static void StopMultiplayerAndClearAllWindows() - { - StopMultiplayer(); - MpUI.ClearWindowStack(); - } - - public static void StopMultiplayer() - { - Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); - - OnMainThread.ClearScheduled(); - LongEventHandler.ClearQueuedEvents(); - - if (session != null) - { - session.Stop(); - session = null; - - Prefs.Apply(); - } - - if (LocalServer != null) - { - LocalServer.running = false; - localServerThread?.Join(); - LocalServer.TryStop(); - LocalServer = null; - } - - game?.OnDestroy(); - game = null; - - TickPatch.Reset(); - VTRSync.Reset(); - - Find.WindowStack?.WindowOfType()?.Cleanup(true); - SyncFieldUtil.ClearAllBufferedChanges(); - - if (arbiterInstance) - { - arbiterInstance = false; - Application.Quit(); - } - } - - private static IEnumerable RwAllAssemblies() - { - yield return Assembly.GetAssembly(typeof(Game)); - - foreach (ModContentPack mod in LoadedModManager.RunningMods) - foreach (Assembly assembly in mod.assemblies.loadedAssemblies) - yield return assembly; - - if (Assembly.GetEntryAssembly() != null) - yield return Assembly.GetEntryAssembly(); - } - } -} + } + + private static void CheckInterfaceVersions() + { + var mpAssembly = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Multiplayer"); + var curVersion = new Version( + (mpAssembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0] as AssemblyFileVersionAttribute).Version + ); + + Log.Message($"Current MultiplayerAPI version: {curVersion}"); + + foreach (var mod in LoadedModManager.RunningMods) { + if (mod.assemblies.loadedAssemblies.NullOrEmpty()) + continue; + + if (mod == modContentPack) + continue; + + // Test if mod is using multiplayer api + if (!mod.assemblies.loadedAssemblies.Any(a => a.GetName().Name == MpVersion.ApiAssemblyName)) { + continue; + } + + // Retrieve the original dll + var info = MultiplayerData.GetModAssemblies(mod) + .Select(f => FileVersionInfo.GetVersionInfo(f.FullName)) + .FirstOrDefault(v => v.ProductName == "Multiplayer"); + + if (info == null) { + // There are certain mods that don't include the API, namely compat + // Can we test them? + continue; + } + + var version = new Version(info.FileVersion); + + Log.Message($"Mod {mod.Name} has MultiplayerAPI client ({version})"); + + if (curVersion > version) + Log.Warning($"Mod {mod.Name} uses an older API version (mod: {version}, current: {curVersion})"); + else if (curVersion < version) + Log.Error($"Mod {mod.Name} uses a newer API version! (mod: {version}, current: {curVersion})\nMake sure the Multiplayer mod is up to date"); + } + } + + public static void StopMultiplayerAndClearAllWindows() + { + StopMultiplayer(); + MpUI.ClearWindowStack(); + } + + public static void StopMultiplayer() + { + Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); + + OnMainThread.ClearScheduled(); + LongEventHandler.ClearQueuedEvents(); + + if (session != null) + { + session.Stop(); + session = null; + + Prefs.Apply(); + } + + if (LocalServer != null) + { + LocalServer.running = false; + localServerThread?.Join(); + LocalServer.TryStop(); + LocalServer = null; + } + + game?.OnDestroy(); + game = null; + + TickPatch.Reset(); + VTRSync.Reset(); + + Find.WindowStack?.WindowOfType()?.Cleanup(true); + SyncFieldUtil.ClearAllBufferedChanges(); + + if (arbiterInstance) + { + arbiterInstance = false; + Application.Quit(); + } + } + + private static IEnumerable RwAllAssemblies() + { + yield return Assembly.GetAssembly(typeof(Game)); + + foreach (ModContentPack mod in LoadedModManager.RunningMods) + foreach (Assembly assembly in mod.assemblies.loadedAssemblies) + yield return assembly; + + if (Assembly.GetEntryAssembly() != null) + yield return Assembly.GetEntryAssembly(); + } + } +} diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index 26cdeb7b1..b021d98dd 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -1,84 +1,85 @@ - - - - net48 - true - 12 - false - bin - false - false - 0.6.2 - false - Multiplayer.Client - Multiplayer - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\ - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - - - - - - - - - - - - - - - - - + + + + net48 + net10.0 + true + 12 + false + bin + false + false + 0.6.2 + false + Multiplayer.Client + Multiplayer + + + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\ + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + + + + + + + + + + + + + + + + + diff --git a/Source/Client/Patches/GravshipTravelSessionPatches.cs b/Source/Client/Patches/GravshipTravelSessionPatches.cs index 4939f821c..8b5e84b4f 100644 --- a/Source/Client/Patches/GravshipTravelSessionPatches.cs +++ b/Source/Client/Patches/GravshipTravelSessionPatches.cs @@ -1,258 +1,258 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using HarmonyLib; -using Multiplayer.API; -using Multiplayer.Client.Persistent; -using Multiplayer.Client.Util; -using RimWorld; -using RimWorld.Planet; -using Verse; -using Verse.Sound; - -namespace Multiplayer.Client.Patches -{ - #region Input - - [HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation))] - public static class PatchGravshipPreLaunchConfirmation - { - static void Prefix(Building_GravEngine engine, ref Action launchAction) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.OpenSessionAt(engine.Map.Tile); - } - } - - [HarmonyPatch] - public static class PatchGravshipPreLaunchCancel - { - static MethodBase TargetMethod() - { - return MpMethodUtil.GetLambda(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation), lambdaOrdinal: 4); - } - - static void Postfix(Dialog_MessageBox __instance) - { - if (Multiplayer.Client == null) return; - if (!Multiplayer.ExecutingCmds) return; - - GravshipTravelUtils.CloseSessionAt(Find.CurrentMap.Tile); - GravshipTravelUtils.CloseGravshipPrelaunchDialog(); - } - } - - [HarmonyPatch(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp))] - public static class PatchPilotConsoleStartChoosingDestination - { - static void Postfix(CompPilotConsole __instance, bool launching) - { - if (Multiplayer.Client == null) return; - if (!launching) return; - - GravshipTravelUtils.CloseGravshipPrelaunchDialog(); - GravshipTravelUtils.OpenSessionAt(__instance.engine.Map.Tile); - } - } - - [HarmonyPatch] - static class PatchTilePickerCancelLambda - { - static MethodBase TargetMethod() - { - return MpMethodUtil.GetLambda(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp), lambdaOrdinal: 4); - } - - static void Prefix(bool ___launching, ref bool __state) - { - if (Multiplayer.Client == null || ___launching || Multiplayer.dontSync) - return; - - Multiplayer.dontSync = true; - __state = true; - } - - // TODO: Something in Feedback.cs seems to block the wantedMode switch. - // For now, it's set manually - consider keeping it this way permanently. - static void Finalizer(bool ___launching, PlanetTile ___curTile, bool __state) - { - if (Multiplayer.Client == null) return; - if (__state) Multiplayer.dontSync = false; - if (!Multiplayer.ExecutingCmds || !___launching) return; - - Find.World.renderer.wantedMode = WorldRenderMode.None; - Find.TilePicker.StopTargetingInt(); - GravshipTravelUtils.CloseSessionAt(___curTile); - } - } - - [HarmonyPatch] - public static class PatchGravshipMapArriveMethods - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveExistingMap)); - yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveNewMap)); - } - static void Postfix(Gravship gravship) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.OpenSessionAt(gravship.destinationTile); - } - } - - [HarmonyPatch(typeof(Designator_MoveGravship), nameof(Designator_MoveGravship.DesignateSingleCell))] - public static class PatchGravshipDesignatorDeselectForAllClients - { - static void Prefix() => CancelDesignatorDeselection.EnableCanceling(); - - static void Finalizer() => CancelDesignatorDeselection.DisableCanceling(); - } - - // TODO: Is there a better way to synchronize this method? - [HarmonyPatch(typeof(GravshipLandingMarker), nameof(GravshipLandingMarker.BeginLanding))] - public static class PatchBeginLandingToSyncWithClients - { - static bool Prefix(GravshipLandingMarker __instance) - { - if (Multiplayer.Client == null) return true; - if (Multiplayer.ExecutingCmds) return true; - - SyncBeginLanding(__instance); - - return false; - } - - [SyncMethod] - public static void SyncBeginLanding(GravshipLandingMarker landingMarker) - { - var gravshipController = Find.GravshipController; - - if (landingMarker == null || landingMarker.Tile == null) - { - MpLog.Error($"[MP] SyncConfirmGravshipLanding: Marker [{landingMarker != null}] Tile [{landingMarker?.Tile != null}]."); - return; - } - - gravshipController.landingMarker = landingMarker; - landingMarker.BeginLanding(gravshipController); - gravshipController.landingMarker = null; - - if (!TickPatch.currentExecutingCmdIssuedBySelf) - SoundDefOf.Gravship_Land.PlayOneShotOnCamera(); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.AbortLanding))] - public static class PatchAbortLandingToCloseSession - { - static void Prefix(WorldComponent_GravshipController __instance, ref bool __state) - { - if (Multiplayer.Client == null) return; - if (!Multiplayer.ExecutingCmds) return; - - GravshipTravelUtils.CloseSessionAt(__instance.landingTile); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] - public static class PatchPlaceGravshipToUpdateSystemsAfterTheySpawn - { - static void Postfix(Map map) - { - if (Multiplayer.Client == null) return; - - map.glowGrid.GlowGridUpdate_First(); - } - } - - #endregion - - #region Landing/Takeoff freeze - - [HarmonyPatch] - public static class PatchGravshipCutsceneToFreeze - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateTakeoff)); - yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateLanding)); - } - +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using Multiplayer.API; +using Multiplayer.Client.Persistent; +using Multiplayer.Client.Util; +using RimWorld; +using RimWorld.Planet; +using Verse; +using Verse.Sound; + +namespace Multiplayer.Client.Patches +{ + #region Input + + [HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation))] + public static class PatchGravshipPreLaunchConfirmation + { + static void Prefix(Building_GravEngine engine, ref Action launchAction) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.OpenSessionAt(engine.Map.Tile); + } + } + + [HarmonyPatch] + public static class PatchGravshipPreLaunchCancel + { + static MethodBase TargetMethod() + { + return MpMethodUtil.GetLambda(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation), lambdaOrdinal: 4); + } + static void Postfix() - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.StartFreeze(); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.TakeoffEnded))] - public static class PatchGravshipTakeoffEnded - { - static void Prefix(WorldComponent_GravshipController __instance) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.StopFreeze(); - GravshipTravelUtils.CloseSessionAt(__instance.takeoffTile); - } - } - - // TODO: Is the random pushing here necessary? This might be related to issue #638. - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.LandingEnded))] - public static class PatchGravshipLandingEnded - { - static void Prefix(WorldComponent_GravshipController __instance) - { - if (Multiplayer.Client == null) return; - - Rand.PushState(); - Rand.StateCompressed = __instance.map.AsyncTime().randState; - - GravshipTravelUtils.StopFreeze(); - GravshipTravelUtils.CloseSessionAt(__instance.gravship.destinationTile); - - // LandingEnded calls Find.TickManager.CurTimeSpeed = TimeSpeed.Normal, but that doesn't work in MP as we - // have our own way of changing the map speed. - __instance.map.AsyncTime().DesiredTimeSpeed = TimeSpeed.Normal; - } - - static void Finalizer() - { - if (Multiplayer.Client == null) return; - Rand.PopState(); - } - } - #endregion - - // TODO: Check what this actually does and whether it’s still necessary - // Stop the landing co message from showing every game tick - [HarmonyPatch(typeof(TickManager), nameof(TickManager.PlayerCanControl), MethodType.Getter)] - public static class PatchTickmanagerPlayerCanControlGetter - { - private static bool shownLandingMessage = false; - - static bool Prefix(ref AcceptanceReport __result) - { - if (Multiplayer.Client == null) return true; - - WorldComponent_GravshipController gravshipController = Find.GravshipController; - if (gravshipController != null && gravshipController.LandingAreaConfirmationInProgress) - { - __result = shownLandingMessage ? false : "MessageConfirmLandingAreaFirst".Translate(); - shownLandingMessage = true; - return false; - } - - // Not in a landing session, use vanilla logic for player control - __result = Current.Game.PlayerHasControl; - return false; - } - - // Call this when the landing session ends (e.g., in your session cleanup) - public static void ResetLandingMessageFlag() => shownLandingMessage = false; - } -} + { + if (Multiplayer.Client == null) return; + if (!Multiplayer.ExecutingCmds) return; + + GravshipTravelUtils.CloseSessionAt(Find.CurrentMap.Tile); + GravshipTravelUtils.CloseGravshipPrelaunchDialog(); + } + } + + [HarmonyPatch(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp))] + public static class PatchPilotConsoleStartChoosingDestination + { + static void Postfix(CompPilotConsole __instance, bool launching) + { + if (Multiplayer.Client == null) return; + if (!launching) return; + + GravshipTravelUtils.CloseGravshipPrelaunchDialog(); + GravshipTravelUtils.OpenSessionAt(__instance.engine.Map.Tile); + } + } + + [HarmonyPatch] + static class PatchTilePickerCancelLambda + { + static MethodBase TargetMethod() + { + return MpMethodUtil.GetLambda(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp), lambdaOrdinal: 4); + } + + static void Prefix(bool ___launching, ref bool __state) + { + if (Multiplayer.Client == null || ___launching || Multiplayer.dontSync) + return; + + Multiplayer.dontSync = true; + __state = true; + } + + // TODO: Something in Feedback.cs seems to block the wantedMode switch. + // For now, it's set manually - consider keeping it this way permanently. + static void Finalizer(bool ___launching, PlanetTile ___curTile, bool __state) + { + if (Multiplayer.Client == null) return; + if (__state) Multiplayer.dontSync = false; + if (!Multiplayer.ExecutingCmds || !___launching) return; + + Find.World.renderer.wantedMode = WorldRenderMode.None; + Find.TilePicker.StopTargetingInt(); + GravshipTravelUtils.CloseSessionAt(___curTile); + } + } + + [HarmonyPatch] + public static class PatchGravshipMapArriveMethods + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveExistingMap)); + yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveNewMap)); + } + static void Postfix(Gravship gravship) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.OpenSessionAt(gravship.destinationTile); + } + } + + [HarmonyPatch(typeof(Designator_MoveGravship), nameof(Designator_MoveGravship.DesignateSingleCell))] + public static class PatchGravshipDesignatorDeselectForAllClients + { + static void Prefix() => CancelDesignatorDeselection.EnableCanceling(); + + static void Finalizer() => CancelDesignatorDeselection.DisableCanceling(); + } + + // TODO: Is there a better way to synchronize this method? + [HarmonyPatch(typeof(GravshipLandingMarker), nameof(GravshipLandingMarker.BeginLanding))] + public static class PatchBeginLandingToSyncWithClients + { + static bool Prefix(GravshipLandingMarker __instance) + { + if (Multiplayer.Client == null) return true; + if (Multiplayer.ExecutingCmds) return true; + + SyncBeginLanding(__instance); + + return false; + } + + [SyncMethod] + public static void SyncBeginLanding(GravshipLandingMarker landingMarker) + { + var gravshipController = Find.GravshipController; + + if (landingMarker == null || landingMarker.Tile == null) + { + MpLog.Error($"[MP] SyncConfirmGravshipLanding: Marker [{landingMarker != null}] Tile [{landingMarker?.Tile != null}]."); + return; + } + + gravshipController.landingMarker = landingMarker; + landingMarker.BeginLanding(gravshipController); + gravshipController.landingMarker = null; + + if (!TickPatch.currentExecutingCmdIssuedBySelf) + SoundDefOf.Gravship_Land.PlayOneShotOnCamera(); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.AbortLanding))] + public static class PatchAbortLandingToCloseSession + { + static void Prefix(WorldComponent_GravshipController __instance, ref bool __state) + { + if (Multiplayer.Client == null) return; + if (!Multiplayer.ExecutingCmds) return; + + GravshipTravelUtils.CloseSessionAt(__instance.landingTile); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] + public static class PatchPlaceGravshipToUpdateSystemsAfterTheySpawn + { + static void Postfix(Map map) + { + if (Multiplayer.Client == null) return; + + map.glowGrid.GlowGridUpdate_First(); + } + } + + #endregion + + #region Landing/Takeoff freeze + + [HarmonyPatch] + public static class PatchGravshipCutsceneToFreeze + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateTakeoff)); + yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateLanding)); + } + + static void Postfix() + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.StartFreeze(); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.TakeoffEnded))] + public static class PatchGravshipTakeoffEnded + { + static void Prefix(WorldComponent_GravshipController __instance) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.StopFreeze(); + GravshipTravelUtils.CloseSessionAt(__instance.takeoffTile); + } + } + + // TODO: Is the random pushing here necessary? This might be related to issue #638. + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.LandingEnded))] + public static class PatchGravshipLandingEnded + { + static void Prefix(WorldComponent_GravshipController __instance) + { + if (Multiplayer.Client == null) return; + + Rand.PushState(); + Rand.StateCompressed = __instance.map.AsyncTime().randState; + + GravshipTravelUtils.StopFreeze(); + GravshipTravelUtils.CloseSessionAt(__instance.gravship.destinationTile); + + // LandingEnded calls Find.TickManager.CurTimeSpeed = TimeSpeed.Normal, but that doesn't work in MP as we + // have our own way of changing the map speed. + __instance.map.AsyncTime().DesiredTimeSpeed = TimeSpeed.Normal; + } + + static void Finalizer() + { + if (Multiplayer.Client == null) return; + Rand.PopState(); + } + } + #endregion + + // TODO: Check what this actually does and whether it’s still necessary + // Stop the landing co message from showing every game tick + [HarmonyPatch(typeof(TickManager), nameof(TickManager.PlayerCanControl), MethodType.Getter)] + public static class PatchTickmanagerPlayerCanControlGetter + { + private static bool shownLandingMessage = false; + + static bool Prefix(ref AcceptanceReport __result) + { + if (Multiplayer.Client == null) return true; + + WorldComponent_GravshipController gravshipController = Find.GravshipController; + if (gravshipController != null && gravshipController.LandingAreaConfirmationInProgress) + { + __result = shownLandingMessage ? false : "MessageConfirmLandingAreaFirst".Translate(); + shownLandingMessage = true; + return false; + } + + // Not in a landing session, use vanilla logic for player control + __result = Current.Game.PlayerHasControl; + return false; + } + + // Call this when the landing session ends (e.g., in your session cleanup) + public static void ResetLandingMessageFlag() => shownLandingMessage = false; + } +} diff --git a/Source/Client/Patches/VanillaTweaks.cs b/Source/Client/Patches/VanillaTweaks.cs index a83a1a147..25292b70e 100644 --- a/Source/Client/Patches/VanillaTweaks.cs +++ b/Source/Client/Patches/VanillaTweaks.cs @@ -1,149 +1,151 @@ -using HarmonyLib; -using RimWorld; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client -{ - // Fixes a lag spike when opening debug tools - [HarmonyPatch(typeof(UIRoot))] - [HarmonyPatch(nameof(UIRoot.UIRootOnGUI))] - static class UIRootPatch - { - static bool done; - - static void Prefix() - { - if (done) return; +using HarmonyLib; +using RimWorld; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client +{ + // Fixes a lag spike when opening debug tools + [HarmonyPatch(typeof(UIRoot))] + [HarmonyPatch(nameof(UIRoot.UIRootOnGUI))] + static class UIRootPatch + { + static bool done; + + static void Prefix() + { + if (done) return; GUI.skin.font = Text.fontStyles[1].font; +#if !RIMWORLD_1_7 Text.fontStyles[1].font.fontNames = new[] { "arial", "arialbd", "ariali", "arialbi" }; - done = true; - } - } - - // Fix window focus handling - [HarmonyPatch(typeof(WindowStack))] - [HarmonyPatch(nameof(WindowStack.CloseWindowsBecauseClicked))] - public static class WindowFocusPatch - { - static void Prefix(WindowStack __instance, Window clickedWindow) - { - for (int i = Find.WindowStack.Windows.Count - 1; i >= 0; i--) - { - Window window = Find.WindowStack.Windows[i]; - __instance.focusedWindow = window; - - if (window == clickedWindow || window.closeOnClickedOutside) return; - UI.UnfocusCurrentControl(); - } - - __instance.focusedWindow = null; - } - } - - [HarmonyPatch(typeof(IncidentWorker_PawnsArrive), nameof(IncidentWorker_PawnsArrive.FactionCanBeGroupSource))] - static class FactionCanBeGroupSourcePatch - { - static void Postfix(Faction f, ref bool __result) - { - __result &= f.def.pawnGroupMakers?.Count > 0; - } - } - - [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.AddNewImmediateWindow))] - static class LongEventWindowPreventCameraMotion - { - public const int LongEventWindowId = 62893994; - - // ReSharper disable once InconsistentNaming - static void Postfix(int ID) - { - if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) - { - var window = Find.WindowStack.windows.Find(w => w.ID == ID); - - window.absorbInputAroundWindow = true; - window.preventCameraMotion = true; - } - } - } - - [HarmonyPatch(typeof(Window), nameof(Window.WindowOnGUI))] - static class WindowDrawDarkBackground - { - static void Prefix(Window __instance) - { - if (Current.ProgramState == ProgramState.Entry) return; - - if (__instance.ID == -LongEventWindowPreventCameraMotion.LongEventWindowId || - __instance.ID == -IngameModal.ModalWindowId || - __instance is DisconnectedWindow || - __instance is CaravanFormingProxy - ) - Widgets.DrawBoxSolid(new Rect(0, 0, UI.screenWidth, UI.screenHeight), new Color(0, 0, 0, 0.5f)); - } - } - - // Fixes a bug with long event handler's immediate window draw order - [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.ImmediateWindow))] - static class AddImmediateWindowsDuringLayouting - { - static IEnumerable Transpiler(IEnumerable insts) - { - bool found = false; - - foreach (var inst in insts) - { - if (!found && inst.opcode == OpCodes.Ldc_I4_7) - { - yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AddImmediateWindowsDuringLayouting), nameof(Process))); - found = true; - } - - yield return inst; - } - } - - static EventType Process(EventType type) => type == EventType.Layout ? EventType.Repaint : type; - } - - [HarmonyPatch(typeof(Widgets), nameof(Widgets.DrawLine))] - static class DrawLineOnlyOnRepaint - { - static bool Prefix() => Event.current.type == EventType.Repaint; - } - - [HarmonyPatch] - public static class WidgetsResolveParsePatch - { - static MethodBase TargetMethod() - { - return AccessTools.Method(typeof(Widgets), nameof(Widgets.ResolveParseNow)).MakeGenericMethod(typeof(int)); - } - - // Fix input field handling - static void Prefix(bool force, ref int val, ref string buffer, ref string edited) - { - if (force) - edited = Widgets.ToStringTypedIn(val); - } - } - - // Fix a lag spike when spawning/destroying things which affect the exit grid - [HarmonyPatch(typeof(ExitMapGrid), nameof(ExitMapGrid.Grid), MethodType.Getter)] - static class ExitMapGridPatch - { - static bool Prefix(ExitMapGrid __instance, ref BoolGrid __result) - { - // A copy of the getter without a costly call to MapUsesExitGrid - if (__instance.dirty) - __instance.Rebuild(); - __result = __instance.exitMapGrid; - return false; - } - } - -} +#endif + done = true; + } + } + + // Fix window focus handling + [HarmonyPatch(typeof(WindowStack))] + [HarmonyPatch(nameof(WindowStack.CloseWindowsBecauseClicked))] + public static class WindowFocusPatch + { + static void Prefix(WindowStack __instance, Window clickedWindow) + { + for (int i = Find.WindowStack.Windows.Count - 1; i >= 0; i--) + { + Window window = Find.WindowStack.Windows[i]; + __instance.focusedWindow = window; + + if (window == clickedWindow || window.closeOnClickedOutside) return; + UI.UnfocusCurrentControl(); + } + + __instance.focusedWindow = null; + } + } + + [HarmonyPatch(typeof(IncidentWorker_PawnsArrive), nameof(IncidentWorker_PawnsArrive.FactionCanBeGroupSource))] + static class FactionCanBeGroupSourcePatch + { + static void Postfix(Faction f, ref bool __result) + { + __result &= f.def.pawnGroupMakers?.Count > 0; + } + } + + [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.AddNewImmediateWindow))] + static class LongEventWindowPreventCameraMotion + { + public const int LongEventWindowId = 62893994; + + // ReSharper disable once InconsistentNaming + static void Postfix(int ID) + { + if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) + { + var window = Find.WindowStack.windows.Find(w => w.ID == ID); + + window.absorbInputAroundWindow = true; + window.preventCameraMotion = true; + } + } + } + + [HarmonyPatch(typeof(Window), nameof(Window.WindowOnGUI))] + static class WindowDrawDarkBackground + { + static void Prefix(Window __instance) + { + if (Current.ProgramState == ProgramState.Entry) return; + + if (__instance.ID == -LongEventWindowPreventCameraMotion.LongEventWindowId || + __instance.ID == -IngameModal.ModalWindowId || + __instance is DisconnectedWindow || + __instance is CaravanFormingProxy + ) + Widgets.DrawBoxSolid(new Rect(0, 0, UI.screenWidth, UI.screenHeight), new Color(0, 0, 0, 0.5f)); + } + } + + // Fixes a bug with long event handler's immediate window draw order + [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.ImmediateWindow))] + static class AddImmediateWindowsDuringLayouting + { + static IEnumerable Transpiler(IEnumerable insts) + { + bool found = false; + + foreach (var inst in insts) + { + if (!found && inst.opcode == OpCodes.Ldc_I4_7) + { + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AddImmediateWindowsDuringLayouting), nameof(Process))); + found = true; + } + + yield return inst; + } + } + + static EventType Process(EventType type) => type == EventType.Layout ? EventType.Repaint : type; + } + + [HarmonyPatch(typeof(Widgets), nameof(Widgets.DrawLine))] + static class DrawLineOnlyOnRepaint + { + static bool Prefix() => Event.current.type == EventType.Repaint; + } + + [HarmonyPatch] + public static class WidgetsResolveParsePatch + { + static MethodBase TargetMethod() + { + return AccessTools.Method(typeof(Widgets), nameof(Widgets.ResolveParseNow)).MakeGenericMethod(typeof(int)); + } + + // Fix input field handling + static void Prefix(bool force, ref int val, ref string buffer, ref string edited) + { + if (force) + edited = Widgets.ToStringTypedIn(val); + } + } + + // Fix a lag spike when spawning/destroying things which affect the exit grid + [HarmonyPatch(typeof(ExitMapGrid), nameof(ExitMapGrid.Grid), MethodType.Getter)] + static class ExitMapGridPatch + { + static bool Prefix(ExitMapGrid __instance, ref BoolGrid __result) + { + // A copy of the getter without a costly call to MapUsesExitGrid + if (__instance.dirty) + __instance.Rebuild(); + __result = __instance.exitMapGrid; + return false; + } + } + +} diff --git a/Source/Client/Syncing/SyncTemplates.cs b/Source/Client/Syncing/SyncTemplates.cs index 88a379f93..c01410980 100644 --- a/Source/Client/Syncing/SyncTemplates.cs +++ b/Source/Client/Syncing/SyncTemplates.cs @@ -1,123 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; - -namespace Multiplayer.Client -{ - public static class SyncTemplates - { - public static HarmonyMethod CreateTranspiler() => new HarmonyMethod(m_Transpiler) { - priority = Priority.First - }; - - static bool General(int internalId, object instance, object[] args) - { - if (Multiplayer.ShouldSync) { - Sync.internalIdToSyncMethod[internalId].DoSync(instance, args); - return false; - } - - return true; - } - - static readonly MethodInfo m_General = SymbolExtensions.GetMethodInfo(() => General(0, null, Array.Empty())); - static readonly MethodInfo m_Transpiler = SymbolExtensions.GetMethodInfo(() => Transpiler(null, null, null)); - - static IEnumerable Transpiler(MethodBase original, IEnumerable instructions, ILGenerator gen) - { - int idx; - var label = gen.DefineLabel(); - var parameters = original.GetParameters(); - - idx = 0; - foreach (var pInfo in parameters) { - var argIndex = idx++ + (original.IsStatic ? 0 : 1); - var pType = pInfo.ParameterType; - if (pInfo.IsOut) { +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace Multiplayer.Client +{ + public static class SyncTemplates + { + public static HarmonyMethod CreateTranspiler() => new HarmonyMethod(m_Transpiler) { + priority = Priority.First + }; + + static bool General(int internalId, object instance, object[] args) + { + if (Multiplayer.ShouldSync) { + Sync.internalIdToSyncMethod[internalId].DoSync(instance, args); + return false; + } + + return true; + } + + static readonly MethodInfo m_General = SymbolExtensions.GetMethodInfo(() => General(0, null, Array.Empty())); + static readonly MethodInfo m_Transpiler = SymbolExtensions.GetMethodInfo(() => Transpiler(null, null, null)); + + static IEnumerable Transpiler(MethodBase original, IEnumerable instructions, ILGenerator gen) + { + int idx; + var label = gen.DefineLabel(); + var parameters = original.GetParameters(); + + idx = 0; + foreach (var pInfo in parameters) { + var argIndex = idx++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + if (pInfo.IsOut) { pType = pType.GetElementType(); yield return new CodeInstruction(OpCodes.Ldarg, argIndex); - yield return CreateDefaultCodes(gen, pType).Last(); - if (AccessTools.IsClass(pType)) - yield return new CodeInstruction(OpCodes.Stind_Ref); - if (AccessTools.IsValue(pType)) { - if (pType == typeof(float)) - yield return new CodeInstruction(OpCodes.Stind_R4, (float) 0); - else if (pType == typeof(double)) - yield return new CodeInstruction(OpCodes.Stind_R8, (double) 0); - else if (pType == typeof(long)) - yield return new CodeInstruction(OpCodes.Stind_I8, (long) 0); - else - yield return new CodeInstruction(OpCodes.Stind_I4, 0); - } - } - } - - yield return new CodeInstruction(OpCodes.Ldc_I4, Sync.methodBaseToInternalId[original]); - yield return new CodeInstruction(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0); - - yield return new CodeInstruction(OpCodes.Ldc_I4, parameters.Length); - yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); - - idx = 0; - var arrayIdx = 0; - foreach (var pInfo in parameters) { - var argIndex = idx++ + (original.IsStatic ? 0 : 1); - var pType = pInfo.ParameterType; - yield return new CodeInstruction(OpCodes.Dup); - yield return new CodeInstruction(OpCodes.Ldc_I4, arrayIdx++); - yield return new CodeInstruction(OpCodes.Ldarg, argIndex); - if (pType.IsByRef) { - pType = pType.GetElementType(); - if (pType.IsValueType) { - yield return new CodeInstruction(OpCodes.Ldobj, pType); - yield return new CodeInstruction(OpCodes.Box, pType); - } else - yield return new CodeInstruction(OpCodes.Ldind_Ref, pType); - } else if (pType.IsValueType) - yield return new CodeInstruction(OpCodes.Box, pType); - yield return new CodeInstruction(OpCodes.Stelem_Ref); - } - yield return new CodeInstruction(OpCodes.Call, m_General); - yield return new CodeInstruction(OpCodes.Brtrue, label); - foreach (var code in CreateDefaultCodes(gen, AccessTools.GetReturnedType(original))) - yield return code; - yield return new CodeInstruction(OpCodes.Ret); - - var list = instructions.ToList(); - list.First().labels.Add(label); - foreach (var instruction in list) - yield return instruction; - } - + yield return new CodeInstruction(OpCodes.Initobj, pType); + } + } + + yield return new CodeInstruction(OpCodes.Ldc_I4, Sync.methodBaseToInternalId[original]); + yield return new CodeInstruction(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0); + + yield return new CodeInstruction(OpCodes.Ldc_I4, parameters.Length); + yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); + + idx = 0; + var arrayIdx = 0; + foreach (var pInfo in parameters) { + var argIndex = idx++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + yield return new CodeInstruction(OpCodes.Dup); + yield return new CodeInstruction(OpCodes.Ldc_I4, arrayIdx++); + yield return new CodeInstruction(OpCodes.Ldarg, argIndex); + if (pType.IsByRef) { + pType = pType.GetElementType(); + if (pType.IsValueType) { + yield return new CodeInstruction(OpCodes.Ldobj, pType); + yield return new CodeInstruction(OpCodes.Box, pType); + } else + yield return new CodeInstruction(OpCodes.Ldind_Ref, pType); + } else if (pType.IsValueType) + yield return new CodeInstruction(OpCodes.Box, pType); + yield return new CodeInstruction(OpCodes.Stelem_Ref); + } + yield return new CodeInstruction(OpCodes.Call, m_General); + yield return new CodeInstruction(OpCodes.Brtrue, label); + foreach (var code in CreateDefaultCodes(gen, AccessTools.GetReturnedType(original))) + yield return code; + yield return new CodeInstruction(OpCodes.Ret); + + var list = instructions.ToList(); + list.First().labels.Add(label); + foreach (var instruction in list) + yield return instruction; + } + static IEnumerable CreateDefaultCodes(ILGenerator generator, Type type) { + if (type == typeof(void)) yield break; if (type.IsByRef) type = type.GetElementType(); - if (AccessTools.IsClass(type)) { - yield return new CodeInstruction(OpCodes.Ldnull); - yield break; - } - if (AccessTools.IsStruct(type)) { - var v = generator.DeclareLocal(type); - yield return new CodeInstruction(OpCodes.Ldloca, v); + if (!type.IsValueType) { + yield return new CodeInstruction(OpCodes.Ldnull); + yield break; + } + if (type.IsValueType) { + var v = generator.DeclareLocal(type); + yield return new CodeInstruction(OpCodes.Ldloca, v); yield return new CodeInstruction(OpCodes.Initobj, type); + yield return new CodeInstruction(OpCodes.Ldloc, v); yield break; } - if (AccessTools.IsValue(type)) { - if (type == typeof(float)) - yield return new CodeInstruction(OpCodes.Ldc_R4, (float) 0); - else if (type == typeof(double)) - yield return new CodeInstruction(OpCodes.Ldc_R8, (double) 0); - else if (type == typeof(long)) - yield return new CodeInstruction(OpCodes.Ldc_I8, (long) 0); - else - yield return new CodeInstruction(OpCodes.Ldc_I4, 0); - } - } - - } -} + } + + } +} diff --git a/Source/Client/UI/MainMenuAnimation.cs b/Source/Client/UI/MainMenuAnimation.cs index c1810c71f..73f7ad27b 100644 --- a/Source/Client/UI/MainMenuAnimation.cs +++ b/Source/Client/UI/MainMenuAnimation.cs @@ -1,439 +1,440 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using Multiplayer.Client.Util; -using RimWorld; -using UnityEngine; -using Verse; -using Random = System.Random; - -namespace Multiplayer.Client -{ - [HarmonyPatch(typeof(UI_BackgroundMain), nameof(UI_BackgroundMain.DoOverlay))] - [StaticConstructorOnStartup] - static class MainMenuAnimation - { - static Texture2D texture; - - const float r = 2282; // radius of planet - const float cx = r + 80, cy = r + 384; // center coords - const float origWidth = 8000, origHeight = 5000; - const int texWidth = (int)cx + 1871; // max extent in the x direction - const int texHeight = 5000; - - // The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand - // Verse.Rand can't be used here because it isn't thread-safe - private static Random rand = new(SystemInfo.deviceUniqueIdentifier.GetHashCode()); - - private static float newPulseTimer; - - static MainMenuAnimation() - { - using (DeepProfilerWrapper.Section("Multiplayer main menu animation texture gen")) - { - texture = new(texWidth, texHeight, TextureFormat.RGBA32, false); - RegenTexture(); - } - } - - static void Prefix(UI_BackgroundMain __instance, Rect bgRect) - { - if (Input.GetKeyDown(KeyCode.KeypadPeriod)) - DoMainMenuPatch.hideMainMenu = !DoMainMenuPatch.hideMainMenu; - - if (!Multiplayer.settings.showMainMenuAnim - || __instance.overrideBGImage != null && __instance.overrideBGImage != UI_BackgroundMain.BGPlanet) - return; - - if (Input.GetKeyDown(KeyCode.End)) - RegenTexture(); - - GUI.DrawTexture(bgRect with { width = texWidth * bgRect.width / origWidth }, texture, ScaleMode.ScaleToFit); - -#if DEBUG - if (Input.GetKey(KeyCode.LeftShift)) - DrawEdges(bgRect); -#endif - - if (Event.current.type != EventType.Repaint) return; - - newPulseTimer += Time.deltaTime; - if (newPulseTimer > 1 / 12f) - { - var edge = edges[rand.Next(edges.Count)]; - var switchEndpoints = rand.NextDouble() < 0.5; - pulses.Add(new Pulse(switchEndpoints ? edge.v2 : edge.v1, switchEndpoints ? edge.v1 : edge.v2) { starting = true }); - newPulseTimer = 0; - } - - DrawPulses(bgRect); - } - - static void DrawPulses(Rect bgRect) - { - for (int i = pulses.Count - 1; i >= 0; i--) - { - var pulse = pulses[i]; - pulse.progress += Math.Min(Time.deltaTime, 0.05f) * 0.25f / pulse.dist; - - if (pulse.progress > 1f) - { - pulses.RemoveAt(i); - - if (!pulse.EaseOut) - pulses.Add(new Pulse(pulse.end, pulse.nextEnd) - { - ending = rand.NextDouble() < 0.4f - }); - } - - var start = Quaternion.Euler(pulse.start.theta, pulse.start.phi, 0); - var end = Quaternion.Euler(pulse.end.theta, pulse.end.phi, 0); - - var x = pulse.progress; - - var progress = - pulse.EaseIn && pulse.EaseOut ? x < 0.5 ? 2 * x * x : 1 - (float)Math.Pow(-2 * x + 2, 2) / 2 : - pulse.EaseIn ? x * x : - pulse.EaseOut ? 1 - (1 - x) * (1 - x) : - x; - - float theta = Quaternion.Lerp(start, end, progress).eulerAngles.x; - float phi = Quaternion.Lerp(start, end, progress).eulerAngles.y; - - float posX = cx + r * Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad); - float posY = cy + r * Mathf.Sin(theta * Mathf.Deg2Rad); - - const float alphaEase = 0.3f; - var alpha = - pulse.EaseIn && x < alphaEase ? x / alphaEase : - pulse.EaseOut && x > 1f - alphaEase ? (1 - x) / alphaEase : - 1; - - using (MpStyle.Set(new Color(1, 1, 1, alpha))) - GUI.DrawTexture( - new Rect( - GetPos(bgRect, posX, posY), - new(6, 6) - ), - // After changing the game language, RimWorld destroys and reloads all assets but doesn't - // rerun static constructors, so resource fields reference destroyed resources - // The pulses disappear after changing the language - MultiplayerStatic.Pulse - ); - } - } - - static Vert GetNewEnd(Pulse pulse) - { - Vert newEnd = null; - var attempts = 0; - - while (newEnd == null && attempts < 20) - { - var randEdge = edges[rand.Next(edges.Count)]; - var otherVert = randEdge.OtherVert(pulse.end); - if (otherVert != null && otherVert != pulse.start && otherVert != pulse.end) - newEnd = otherVert; - attempts++; - } - - return newEnd; - } - - const float rotate = 35f; - const float maskTheta = 10; - - static void DrawEdges(Rect bgRect) - { - { - var proj1 = r * Project(maskTheta, -90).RotatedBy(rotate) + new Vector2(cx, cy); - var proj2 = r * Project(maskTheta, 90).RotatedBy(rotate) + new Vector2(cx, cy); - Widgets.DrawLine(GetPos(bgRect, proj1.x, proj1.y), GetPos(bgRect, proj2.x, proj2.y), Color.blue, 2f); - } - - foreach (var e in edges) - { - var start = Quaternion.Euler(e.v1.theta, e.v1.phi, 0); - var end = Quaternion.Euler(e.v2.theta, e.v2.phi, 0); - - float prevX = 0; - float prevY = 0; - - for (float d = 0; d <= 1; d += 0.25f) - { - float theta = Quaternion.Lerp(start, end, d).eulerAngles.x; - float phi = Quaternion.Lerp(start, end, d).eulerAngles.y; - - var proj = Project(theta, phi); - float x = cx + r * proj.x; - float y = cy + r * proj.y; - - if (prevX != 0 && prevY != 0) - { - var lineStart = GetPos(bgRect, prevX, prevY); - var lineEnd = GetPos(bgRect, x, y); - - if (d == 0.5f) - MpUI.Label(new Rect(lineStart, new(100, 100)), $"{e.dist}", GameFont.Tiny); - - Widgets.DrawLine(lineStart, lineEnd, Color.red, 2f); - } - - prevX = x; - prevY = y; - } - } - } - - static float SphereDist(float t1, float p1, float t2, float p2) - { - return Mathf.Acos( - Mathf.Sin(t1 * Mathf.Deg2Rad) * Mathf.Sin(t2 * Mathf.Deg2Rad) - + Mathf.Cos(t1 * Mathf.Deg2Rad) * Mathf.Cos(t2 * Mathf.Deg2Rad) * Mathf.Cos((p2 - p1) * Mathf.Deg2Rad) - ); - } - - private static List verts = new(); - private static List edges = new(); - private static List pulses = new(); - - static void Mst() - { - foreach (var v in verts) - { - v.cost = float.PositiveInfinity; - v.costVert = null; - } - - var q = verts.ToList(); - - while (q.Count > 0) - { - var v = q.MinBy(a => a.cost); - q.Remove(v); - - if (v.costVert != null) - edges.Add(new Edge(v, v.costVert)); - - foreach (var w in q) - { - var dist = SphereDist(v.theta, v.phi, w.theta, w.phi); - if (dist < w.cost) - { - w.cost = dist; - w.costVert = v; - } - } - } - } - - static Vector2 InverseSpherical(float x, float y, float r) - { - var ro = new Vector2(x, y).magnitude; - var c = Mathf.Asin(ro / r); - return new Vector2( - Mathf.Asin(y * Mathf.Sin(c) / ro) * Mathf.Rad2Deg, - Mathf.Atan2(x * Mathf.Sin(c), ro * Mathf.Cos(c)) * Mathf.Rad2Deg - ); - } - - static Vector2 GetPos(Rect rect, float x, float y) - { - return rect.min + new Vector2(rect.width * x / origWidth, rect.height * y / origHeight); - } - - static Vector2 Project(float theta, float phi) - { - return new Vector2( - Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad), - Mathf.Sin(theta * Mathf.Deg2Rad) - ); - } - - static void RegenTexture() - { - using var _ = DeepProfilerWrapper.Section("RegenTexture"); - - verts.Clear(); - - // vaniller cities - verts.Add(new Vert { theta = 60, phi = 34 }); - verts.Add(new Vert { theta = -4, phi = -68 }); - verts.Add(new Vert { theta = -2, phi = -68 }); - verts.Add(new Vert { theta = 4, phi = -50 }); - verts.Add(new Vert { theta = 18, phi = -61 }); - - edges.Clear(); - pulses.Clear(); - - var colors = new Color[texWidth * texHeight]; - - // Cities - for (int i = 0; i < 20;) - { - float cityTheta = rand.Range(maskTheta, 75f); - float cityPhi = rand.Range(-75f, 75f); - - // ugh - var pr = Project(cityTheta, cityPhi).RotatedBy(rotate); - var inv = InverseSpherical(pr.x, pr.y, 1f); - cityTheta = inv.x; - cityPhi = inv.y; - - // Avoid the light side - var cityXY = r * Project(cityTheta, cityPhi) + new Vector2(cx, cy); - const float maskR = 2160; - if ((new Vector2(520 + maskR, 64 + maskR) - cityXY).magnitude < maskR) - continue; - - var blobs = rand.Next(2, 5); - if (rand.NextDouble() < 0.15) blobs += rand.Next(2, 4); - - Vector2? lit = null; - - // Blobs - for (int k = 0; k < blobs; k++) - { - var blobSpan = 2.5f + Math.Max(0, (blobs - 3) * 0.25f); - float theta = cityTheta + rand.Range(-blobSpan, blobSpan); - float phi = cityPhi + rand.Range(-blobSpan, blobSpan); - - // Dots - for (int j = 0; j < 7; j++) - { - var blobSize = j == 0 || rand.NextDouble() < 0.3f ? 15 : 10; - - float dotTheta = rand.Range(-1f, 1f); - float dotPhi = rand.Range(-1f, 1f); - - for (int y = -blobSize; y <= blobSize; y++) - for (int x = -blobSize; x <= blobSize; x++) - if (x * x + y * y < blobSize * blobSize) - { - var proj = Project(theta + dotTheta, phi + dotPhi); - float centerX = cx + r * proj.x; - float centerY = cy + r * proj.y; - - var c = 1 - new Vector2(x,y).magnitude / blobSize; - var fromCenter = 1 - new Vector2(dotTheta, dotPhi).magnitude / 2; - var fromCenter2 = 1 - new Vector2(theta - cityTheta, phi - cityPhi).magnitude / 5; - var a = c * fromCenter * fromCenter2; - - if (j == 0) a *= 3f; - else if (a > 0.4) a *= 2f; - - var pos = new Vector2(centerX, centerY); - var color = new Color(0.93f, 0.75f, 0.55f, a * a * 2); - var pixelX = (int)pos.x + x; - var pixelY = (int)origHeight - (int)pos.y + y; - colors[pixelX + pixelY * texWidth] = color; - - if (color.a >= 1 && lit == null) - { - lit = new Vector2(theta + dotTheta, phi + dotPhi); - } - } - } - } - - if (lit != null) - verts.Add(new Vert { theta = lit.Value.x, phi = lit.Value.y }); - - i++; - } - - using (DeepProfilerWrapper.Section("SetPixels and Apply")) - { - texture.SetPixels(colors); - texture.Apply(); - } - - Mst(); - } - - class Vert - { - public float theta; - public float phi; - - public float cost; - public Vert costVert; - } - - class Edge - { - public readonly Vert v1, v2; - public readonly float dist; - - public Edge(Vert v1, Vert v2) - { - this.v1 = v1; - this.v2 = v2; - - dist = SphereDist(v1.theta, v1.phi, v2.theta, v2.phi); - } - - protected bool Equals(Edge other) - { - return - Equals(v1, other.v1) && Equals(v2, other.v2) - || Equals(v1, other.v2) && Equals(v2, other.v1); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Edge)obj); - } - - public override int GetHashCode() - { - return v1.GetHashCode() * v2.GetHashCode(); - } - - public Vert OtherVert(Vert v) - { - return v1 == v ? v2 : v2 == v ? v1 : null; - } - } - - class Pulse - { - public float progress; - public Vert start; - public Vert end; - public float dist; - - public bool starting; - public bool ending; - public Vert nextEnd; - - public bool EaseIn => starting; - public bool EaseOut => ending || nextEnd == null; - - public Pulse(Vert start, Vert end) - { - this.start = start; - this.end = end; - - dist = SphereDist(start.theta, start.phi, end.theta, end.phi); - nextEnd = GetNewEnd(this); - } - } - } - - [HarmonyPatch(typeof(UIRoot_Entry), nameof(UIRoot_Entry.ShouldDoMainMenu), MethodType.Getter)] - static class DoMainMenuPatch - { - public static bool hideMainMenu; - - static void Postfix(ref bool __result) - { - if (hideMainMenu) - __result = false; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Client.Util; +using RimWorld; +using UnityEngine; +using Verse; +using Random = System.Random; + +namespace Multiplayer.Client +{ + [HarmonyPatch(typeof(UI_BackgroundMain), nameof(UI_BackgroundMain.DoOverlay))] + [StaticConstructorOnStartup] + static class MainMenuAnimation + { + static Texture2D texture; + + const float r = 2282; // radius of planet + const float cx = r + 80, cy = r + 384; // center coords + const float origWidth = 8000, origHeight = 5000; + const int texWidth = (int)cx + 1871; // max extent in the x direction + const int texHeight = 5000; + + // The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand + // Verse.Rand can't be used here because it isn't thread-safe + // Cosmetic only: no dependency on Unity's hardware identifier. + private static Random rand = new(); + + private static float newPulseTimer; + + static MainMenuAnimation() + { + using (DeepProfilerWrapper.Section("Multiplayer main menu animation texture gen")) + { + texture = new(texWidth, texHeight, TextureFormat.RGBA32, false); + RegenTexture(); + } + } + + static void Prefix(UI_BackgroundMain __instance, Rect bgRect) + { + if (Input.GetKeyDown(KeyCode.KeypadPeriod)) + DoMainMenuPatch.hideMainMenu = !DoMainMenuPatch.hideMainMenu; + + if (!Multiplayer.settings.showMainMenuAnim + || __instance.overrideBGImage != null && __instance.overrideBGImage != UI_BackgroundMain.BGPlanet) + return; + + if (Input.GetKeyDown(KeyCode.End)) + RegenTexture(); + + GUI.DrawTexture(bgRect with { width = texWidth * bgRect.width / origWidth }, texture, ScaleMode.ScaleToFit); + +#if DEBUG + if (Input.GetKey(KeyCode.LeftShift)) + DrawEdges(bgRect); +#endif + + if (Event.current.type != EventType.Repaint) return; + + newPulseTimer += Time.deltaTime; + if (newPulseTimer > 1 / 12f) + { + var edge = edges[rand.Next(edges.Count)]; + var switchEndpoints = rand.NextDouble() < 0.5; + pulses.Add(new Pulse(switchEndpoints ? edge.v2 : edge.v1, switchEndpoints ? edge.v1 : edge.v2) { starting = true }); + newPulseTimer = 0; + } + + DrawPulses(bgRect); + } + + static void DrawPulses(Rect bgRect) + { + for (int i = pulses.Count - 1; i >= 0; i--) + { + var pulse = pulses[i]; + pulse.progress += Math.Min(Time.deltaTime, 0.05f) * 0.25f / pulse.dist; + + if (pulse.progress > 1f) + { + pulses.RemoveAt(i); + + if (!pulse.EaseOut) + pulses.Add(new Pulse(pulse.end, pulse.nextEnd) + { + ending = rand.NextDouble() < 0.4f + }); + } + + var start = Quaternion.Euler(pulse.start.theta, pulse.start.phi, 0); + var end = Quaternion.Euler(pulse.end.theta, pulse.end.phi, 0); + + var x = pulse.progress; + + var progress = + pulse.EaseIn && pulse.EaseOut ? x < 0.5 ? 2 * x * x : 1 - (float)Math.Pow(-2 * x + 2, 2) / 2 : + pulse.EaseIn ? x * x : + pulse.EaseOut ? 1 - (1 - x) * (1 - x) : + x; + + float theta = Quaternion.Lerp(start, end, progress).eulerAngles.x; + float phi = Quaternion.Lerp(start, end, progress).eulerAngles.y; + + float posX = cx + r * Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad); + float posY = cy + r * Mathf.Sin(theta * Mathf.Deg2Rad); + + const float alphaEase = 0.3f; + var alpha = + pulse.EaseIn && x < alphaEase ? x / alphaEase : + pulse.EaseOut && x > 1f - alphaEase ? (1 - x) / alphaEase : + 1; + + using (MpStyle.Set(new Color(1, 1, 1, alpha))) + GUI.DrawTexture( + new Rect( + GetPos(bgRect, posX, posY), + new(6, 6) + ), + // After changing the game language, RimWorld destroys and reloads all assets but doesn't + // rerun static constructors, so resource fields reference destroyed resources + // The pulses disappear after changing the language + MultiplayerStatic.Pulse + ); + } + } + + static Vert GetNewEnd(Pulse pulse) + { + Vert newEnd = null; + var attempts = 0; + + while (newEnd == null && attempts < 20) + { + var randEdge = edges[rand.Next(edges.Count)]; + var otherVert = randEdge.OtherVert(pulse.end); + if (otherVert != null && otherVert != pulse.start && otherVert != pulse.end) + newEnd = otherVert; + attempts++; + } + + return newEnd; + } + + const float rotate = 35f; + const float maskTheta = 10; + + static void DrawEdges(Rect bgRect) + { + { + var proj1 = r * Project(maskTheta, -90).RotatedBy(rotate) + new Vector2(cx, cy); + var proj2 = r * Project(maskTheta, 90).RotatedBy(rotate) + new Vector2(cx, cy); + Widgets.DrawLine(GetPos(bgRect, proj1.x, proj1.y), GetPos(bgRect, proj2.x, proj2.y), Color.blue, 2f); + } + + foreach (var e in edges) + { + var start = Quaternion.Euler(e.v1.theta, e.v1.phi, 0); + var end = Quaternion.Euler(e.v2.theta, e.v2.phi, 0); + + float prevX = 0; + float prevY = 0; + + for (float d = 0; d <= 1; d += 0.25f) + { + float theta = Quaternion.Lerp(start, end, d).eulerAngles.x; + float phi = Quaternion.Lerp(start, end, d).eulerAngles.y; + + var proj = Project(theta, phi); + float x = cx + r * proj.x; + float y = cy + r * proj.y; + + if (prevX != 0 && prevY != 0) + { + var lineStart = GetPos(bgRect, prevX, prevY); + var lineEnd = GetPos(bgRect, x, y); + + if (d == 0.5f) + MpUI.Label(new Rect(lineStart, new(100, 100)), $"{e.dist}", GameFont.Tiny); + + Widgets.DrawLine(lineStart, lineEnd, Color.red, 2f); + } + + prevX = x; + prevY = y; + } + } + } + + static float SphereDist(float t1, float p1, float t2, float p2) + { + return Mathf.Acos( + Mathf.Sin(t1 * Mathf.Deg2Rad) * Mathf.Sin(t2 * Mathf.Deg2Rad) + + Mathf.Cos(t1 * Mathf.Deg2Rad) * Mathf.Cos(t2 * Mathf.Deg2Rad) * Mathf.Cos((p2 - p1) * Mathf.Deg2Rad) + ); + } + + private static List verts = new(); + private static List edges = new(); + private static List pulses = new(); + + static void Mst() + { + foreach (var v in verts) + { + v.cost = float.PositiveInfinity; + v.costVert = null; + } + + var q = verts.ToList(); + + while (q.Count > 0) + { + var v = q.MinBy(a => a.cost); + q.Remove(v); + + if (v.costVert != null) + edges.Add(new Edge(v, v.costVert)); + + foreach (var w in q) + { + var dist = SphereDist(v.theta, v.phi, w.theta, w.phi); + if (dist < w.cost) + { + w.cost = dist; + w.costVert = v; + } + } + } + } + + static Vector2 InverseSpherical(float x, float y, float r) + { + var ro = new Vector2(x, y).magnitude; + var c = Mathf.Asin(ro / r); + return new Vector2( + Mathf.Asin(y * Mathf.Sin(c) / ro) * Mathf.Rad2Deg, + Mathf.Atan2(x * Mathf.Sin(c), ro * Mathf.Cos(c)) * Mathf.Rad2Deg + ); + } + + static Vector2 GetPos(Rect rect, float x, float y) + { + return rect.min + new Vector2(rect.width * x / origWidth, rect.height * y / origHeight); + } + + static Vector2 Project(float theta, float phi) + { + return new Vector2( + Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad), + Mathf.Sin(theta * Mathf.Deg2Rad) + ); + } + + static void RegenTexture() + { + using var _ = DeepProfilerWrapper.Section("RegenTexture"); + + verts.Clear(); + + // vaniller cities + verts.Add(new Vert { theta = 60, phi = 34 }); + verts.Add(new Vert { theta = -4, phi = -68 }); + verts.Add(new Vert { theta = -2, phi = -68 }); + verts.Add(new Vert { theta = 4, phi = -50 }); + verts.Add(new Vert { theta = 18, phi = -61 }); + + edges.Clear(); + pulses.Clear(); + + var colors = new Color[texWidth * texHeight]; + + // Cities + for (int i = 0; i < 20;) + { + float cityTheta = rand.Range(maskTheta, 75f); + float cityPhi = rand.Range(-75f, 75f); + + // ugh + var pr = Project(cityTheta, cityPhi).RotatedBy(rotate); + var inv = InverseSpherical(pr.x, pr.y, 1f); + cityTheta = inv.x; + cityPhi = inv.y; + + // Avoid the light side + var cityXY = r * Project(cityTheta, cityPhi) + new Vector2(cx, cy); + const float maskR = 2160; + if ((new Vector2(520 + maskR, 64 + maskR) - cityXY).magnitude < maskR) + continue; + + var blobs = rand.Next(2, 5); + if (rand.NextDouble() < 0.15) blobs += rand.Next(2, 4); + + Vector2? lit = null; + + // Blobs + for (int k = 0; k < blobs; k++) + { + var blobSpan = 2.5f + Math.Max(0, (blobs - 3) * 0.25f); + float theta = cityTheta + rand.Range(-blobSpan, blobSpan); + float phi = cityPhi + rand.Range(-blobSpan, blobSpan); + + // Dots + for (int j = 0; j < 7; j++) + { + var blobSize = j == 0 || rand.NextDouble() < 0.3f ? 15 : 10; + + float dotTheta = rand.Range(-1f, 1f); + float dotPhi = rand.Range(-1f, 1f); + + for (int y = -blobSize; y <= blobSize; y++) + for (int x = -blobSize; x <= blobSize; x++) + if (x * x + y * y < blobSize * blobSize) + { + var proj = Project(theta + dotTheta, phi + dotPhi); + float centerX = cx + r * proj.x; + float centerY = cy + r * proj.y; + + var c = 1 - new Vector2(x,y).magnitude / blobSize; + var fromCenter = 1 - new Vector2(dotTheta, dotPhi).magnitude / 2; + var fromCenter2 = 1 - new Vector2(theta - cityTheta, phi - cityPhi).magnitude / 5; + var a = c * fromCenter * fromCenter2; + + if (j == 0) a *= 3f; + else if (a > 0.4) a *= 2f; + + var pos = new Vector2(centerX, centerY); + var color = new Color(0.93f, 0.75f, 0.55f, a * a * 2); + var pixelX = (int)pos.x + x; + var pixelY = (int)origHeight - (int)pos.y + y; + colors[pixelX + pixelY * texWidth] = color; + + if (color.a >= 1 && lit == null) + { + lit = new Vector2(theta + dotTheta, phi + dotPhi); + } + } + } + } + + if (lit != null) + verts.Add(new Vert { theta = lit.Value.x, phi = lit.Value.y }); + + i++; + } + + using (DeepProfilerWrapper.Section("SetPixels and Apply")) + { + texture.SetPixels(colors); + texture.Apply(); + } + + Mst(); + } + + class Vert + { + public float theta; + public float phi; + + public float cost; + public Vert costVert; + } + + class Edge + { + public readonly Vert v1, v2; + public readonly float dist; + + public Edge(Vert v1, Vert v2) + { + this.v1 = v1; + this.v2 = v2; + + dist = SphereDist(v1.theta, v1.phi, v2.theta, v2.phi); + } + + protected bool Equals(Edge other) + { + return + Equals(v1, other.v1) && Equals(v2, other.v2) + || Equals(v1, other.v2) && Equals(v2, other.v1); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((Edge)obj); + } + + public override int GetHashCode() + { + return v1.GetHashCode() * v2.GetHashCode(); + } + + public Vert OtherVert(Vert v) + { + return v1 == v ? v2 : v2 == v ? v1 : null; + } + } + + class Pulse + { + public float progress; + public Vert start; + public Vert end; + public float dist; + + public bool starting; + public bool ending; + public Vert nextEnd; + + public bool EaseIn => starting; + public bool EaseOut => ending || nextEnd == null; + + public Pulse(Vert start, Vert end) + { + this.start = start; + this.end = end; + + dist = SphereDist(start.theta, start.phi, end.theta, end.phi); + nextEnd = GetNewEnd(this); + } + } + } + + [HarmonyPatch(typeof(UIRoot_Entry), nameof(UIRoot_Entry.ShouldDoMainMenu), MethodType.Getter)] + static class DoMainMenuPatch + { + public static bool hideMainMenu; + + static void Postfix(ref bool __result) + { + if (hideMainMenu) + __result = false; + } + } +} diff --git a/Source/Client/Util/MpUI.cs b/Source/Client/Util/MpUI.cs index b80ea128d..f055dde2b 100644 --- a/Source/Client/Util/MpUI.cs +++ b/Source/Client/Util/MpUI.cs @@ -1,276 +1,278 @@ -using System.Collections.Generic; -using HarmonyLib; -using RimWorld; -using UnityEngine; -using Verse; -using Verse.Sound; - -namespace Multiplayer.Client.Util -{ - public static class MpUI - { - public static Vector2 Resolution => new Vector2(UI.screenWidth, UI.screenHeight); - - public static string FixedEllipsis() - { - int num = Mathf.FloorToInt(Time.realtimeSinceStartup) % 3; - return num switch - { - 0 => ". ", - 1 => ".. ", - _ => "..." - }; - } - - public static void DrawRotatedLine(Vector2 center, float length, float width, float angle, Color color) - { - var size = new Vector2(length, width); - var start = center - size / 2f; - Rect screenRect = new Rect(start.x, start.y, length, width); - Matrix4x4 m = Matrix4x4.TRS(center, Quaternion.Euler(0f, 0f, angle), Vector3.one) * Matrix4x4.TRS(-center, Quaternion.identity, Vector3.one); - - GL.PushMatrix(); - GL.MultMatrix(m); - GUI.DrawTexture(screenRect, Widgets.LineTexAA, ScaleMode.StretchToFill, true, 0f, color, 0f, 0f); - GL.PopMatrix(); - } - - public static void Label(Rect rect, string label, GameFont? font = null, TextAnchor? anchor = null, Color? color = null) - { - var prevFont = Text.Font; - var prevAnchor = Text.Anchor; - var prevColor = GUI.color; - - if (font != null) - Text.Font = font.Value; - - if (anchor != null) - Text.Anchor = anchor.Value; - - if (color != null) - GUI.color = color.Value; - - Widgets.Label(rect, label); - - GUI.color = prevColor; - Text.Anchor = prevAnchor; - Text.Font = prevFont; - } - - public static void Checkbox(float x, float y, ref bool checkOn, bool disabled, float size) - { - if (!disabled && Widgets.ButtonInvisible(new Rect(x, y, size, size), false)) - { - checkOn = !checkOn; - if (checkOn) - SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); - else - SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); - } - - Widgets.CheckboxDraw(x, y, checkOn, disabled, size); - } - - public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, ElementOrder order = ElementOrder.Apart, float size = 24f) - { - TextAnchor anchor = Text.Anchor; - Text.Anchor = TextAnchor.MiddleLeft; - - if (order == ElementOrder.Right) - { - float textWidth = Text.CalcSize(label).x; - rect.x = rect.xMax - textWidth - size - 5f; - rect.width = textWidth + size + 5f; - } - else if (order == ElementOrder.Left) - { - float textWidth = Text.CalcSize(label).x; - rect.width = textWidth + size + 5f; - } - - Widgets.Label(rect, label); - - Checkbox( - rect.xMax - size, - rect.y + (rect.height - size) / 2, - ref checkOn, - disabled, - size); - - Text.Anchor = anchor; - - return rect; - } - - public static string TextEntryLabeled(Rect rect, string label, string text, float labelWidth) - { - Rect labelRect = rect.Rounded(); - labelRect.width = labelWidth; - - using (MpStyle.Set(TextAnchor.MiddleRight)) - Widgets.Label(labelRect, label); - - rect.xMin += labelWidth; - return Widgets.TextField(rect, text); - } - - public static bool TextFieldNumericLabeled(Rect rect, string label, ref T val, ref string buffer, float labelWidth, float min = 0, float max = float.MaxValue, bool clickable = false, string tooltip = null) where T : struct - { - Rect labelRect = rect; - labelRect.width = labelWidth; - - using (MpStyle.Set(TextAnchor.MiddleRight)) - using (MpStyle.Set(WordWrap.NoWrap)) - Widgets.Label(labelRect, label); - - if (clickable) - Widgets.DrawHighlightIfMouseover(labelRect.MinX(labelRect.xMax - Text.CalcSize(label).x - 10)); - - if (tooltip != null) - TooltipHandler.TipRegion(labelRect, tooltip); - - rect.xMin += labelWidth; - Widgets.TextFieldNumeric(rect, ref val, ref buffer, min, max); - - return clickable && Widgets.ButtonInvisible(labelRect); - } - - public static void ClearWindowStack() - { - Find.WindowStack.windows.Clear(); - } - - public static void TryUnfocusCurrentNamedControl(Window window) - { - // Unfocus the input field after a click or ESC press - if ((Event.current.type == EventType.MouseDown || KeyBindingDefOf.Cancel.KeyDownEvent) && !GUI.GetNameOfFocusedControl().NullOrEmpty()) - { - Event.current.Use(); - UI.UnfocusCurrentControl(); - Find.WindowStack.Notify_ManuallySetFocus(window); - } - } - - public static void LabelWithTip(Rect rect, string str, string tip) - { - Widgets.Label(rect, str); - - if (Mouse.IsOver(rect) && tip != null) - TooltipHandler.TipRegion(rect, new TipSignal(tip, 3333)); - } - - public static void LabelTruncatedWithTip(Rect rect, string str, Dictionary cache) - { - var trunc = str.Truncate(rect.width, cache); - LabelWithTip(rect, trunc, trunc != str ? str : null); - } - - public static void CheckboxLabeledWithTip(Rect rect, string label, string tip, ref bool check, bool disabled = false) - { - Widgets.DrawHighlightIfMouseover(rect); - if (tip != null) - TooltipHandler.TipRegion(rect, tip); - CheckboxLabeled(rect, label, ref check, disabled); - } - - public static void CheckboxLabeledWithTipNoHighlight(Rect rect, string label, string tip, ref bool check, bool disabled = false, bool placeTextNearCheckbox = false) - { - if (tip != null) - TooltipHandler.TipRegion(rect, tip); - CheckboxLabeled(rect, label, ref check, disabled, order: placeTextNearCheckbox ? ElementOrder.Right : ElementOrder.Apart); - } - - public static bool ButtonTextWithTip(Rect rect, string label, string tip, bool disabled = false, int? tipId = null) - { - if (tip != null) - TooltipHandler.TipRegion(rect, new TipSignal(tip, tipId ?? tip.GetHashCode())); - - using (MpStyle.Set(TextAnchor.MiddleCenter)) - using (MpStyle.Set(disabled ? Color.grey : Color.white)) - { - var atlas = !disabled && Mouse.IsOver(rect) ? - (Input.GetMouseButton(0) ? Widgets.ButtonBGAtlasClick : Widgets.ButtonBGAtlasMouseover) : - Widgets.ButtonBGAtlas; - - Widgets.DrawAtlas(rect, atlas); - Widgets.Label(rect, label); - } - - var clicked = Widgets.ButtonInvisible(rect, false); - - if (!disabled) - { - MouseoverSounds.DoRegion(rect); - return clicked; - } - - if (clicked) - SoundDefOf.ClickReject.PlayOneShotOnCamera(); - - return false; - } - - public static float LabelFlexibleWidth(Rect rect, string label) - { - var size = Text.CalcSize(label); - rect.width = size.x; - Widgets.Label(rect, label); - return size.x; - } - - public static float LabelFlexibleWidthClickable(Rect rect, string label, ref bool clicked) - { - var size = Text.CalcSize(label); - rect.width = size.x; - Widgets.DrawHighlightIfMouseover(rect); - clicked = Widgets.ButtonInvisible(rect, false); - Widgets.Label(rect, label); - return size.x; - } - - const char PassChar = '\u2022'; - - public static void DoPasswordField(Rect rect, string controlName, ref string password) - { - password ??= ""; - - TextEditor te = - GUI.GetNameOfFocusedControl() == controlName ? - (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl) : - null; - - Vector2 scrollOffset = default; - bool keyDown = Event.current.type == EventType.KeyDown; - if (te != null) - scrollOffset = te.scrollOffset; - - GUI.SetNextControlName(controlName); - - if (Event.current.type is EventType.Repaint or EventType.MouseDown) - GUI.TextField( - rect, - new string(PassChar, password.Length), - Text.textFieldStyles[1] - ); - else - password = GUI.TextField( - rect, - password, - Text.textFieldStyles[1] - ); - - if (te != null && keyDown) - { - te.scrollOffset = scrollOffset; +using System.Collections.Generic; +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace Multiplayer.Client.Util +{ + public static class MpUI + { + public static Vector2 Resolution => new Vector2(UI.screenWidth, UI.screenHeight); + + public static string FixedEllipsis() + { + int num = Mathf.FloorToInt(Time.realtimeSinceStartup) % 3; + return num switch + { + 0 => ". ", + 1 => ".. ", + _ => "..." + }; + } + + public static void DrawRotatedLine(Vector2 center, float length, float width, float angle, Color color) + { + var size = new Vector2(length, width); + var start = center - size / 2f; + Rect screenRect = new Rect(start.x, start.y, length, width); + Matrix4x4 m = Matrix4x4.TRS(center, Quaternion.Euler(0f, 0f, angle), Vector3.one) * Matrix4x4.TRS(-center, Quaternion.identity, Vector3.one); + + GL.PushMatrix(); + GL.MultMatrix(m); + GUI.DrawTexture(screenRect, Widgets.LineTexAA, ScaleMode.StretchToFill, true, 0f, color, 0f, 0f); + GL.PopMatrix(); + } + + public static void Label(Rect rect, string label, GameFont? font = null, TextAnchor? anchor = null, Color? color = null) + { + var prevFont = Text.Font; + var prevAnchor = Text.Anchor; + var prevColor = GUI.color; + + if (font != null) + Text.Font = font.Value; + + if (anchor != null) + Text.Anchor = anchor.Value; + + if (color != null) + GUI.color = color.Value; + + Widgets.Label(rect, label); + + GUI.color = prevColor; + Text.Anchor = prevAnchor; + Text.Font = prevFont; + } + + public static void Checkbox(float x, float y, ref bool checkOn, bool disabled, float size) + { + if (!disabled && Widgets.ButtonInvisible(new Rect(x, y, size, size), false)) + { + checkOn = !checkOn; + if (checkOn) + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); + else + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); + } + + Widgets.CheckboxDraw(x, y, checkOn, disabled, size); + } + + public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, ElementOrder order = ElementOrder.Apart, float size = 24f) + { + TextAnchor anchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + + if (order == ElementOrder.Right) + { + float textWidth = Text.CalcSize(label).x; + rect.x = rect.xMax - textWidth - size - 5f; + rect.width = textWidth + size + 5f; + } + else if (order == ElementOrder.Left) + { + float textWidth = Text.CalcSize(label).x; + rect.width = textWidth + size + 5f; + } + + Widgets.Label(rect, label); + + Checkbox( + rect.xMax - size, + rect.y + (rect.height - size) / 2, + ref checkOn, + disabled, + size); + + Text.Anchor = anchor; + + return rect; + } + + public static string TextEntryLabeled(Rect rect, string label, string text, float labelWidth) + { + Rect labelRect = rect.Rounded(); + labelRect.width = labelWidth; + + using (MpStyle.Set(TextAnchor.MiddleRight)) + Widgets.Label(labelRect, label); + + rect.xMin += labelWidth; + return Widgets.TextField(rect, text); + } + + public static bool TextFieldNumericLabeled(Rect rect, string label, ref T val, ref string buffer, float labelWidth, float min = 0, float max = float.MaxValue, bool clickable = false, string tooltip = null) where T : struct + { + Rect labelRect = rect; + labelRect.width = labelWidth; + + using (MpStyle.Set(TextAnchor.MiddleRight)) + using (MpStyle.Set(WordWrap.NoWrap)) + Widgets.Label(labelRect, label); + + if (clickable) + Widgets.DrawHighlightIfMouseover(labelRect.MinX(labelRect.xMax - Text.CalcSize(label).x - 10)); + + if (tooltip != null) + TooltipHandler.TipRegion(labelRect, tooltip); + + rect.xMin += labelWidth; + Widgets.TextFieldNumeric(rect, ref val, ref buffer, min, max); + + return clickable && Widgets.ButtonInvisible(labelRect); + } + + public static void ClearWindowStack() + { + Find.WindowStack.windows.Clear(); + } + + public static void TryUnfocusCurrentNamedControl(Window window) + { + // Unfocus the input field after a click or ESC press + if ((Event.current.type == EventType.MouseDown || KeyBindingDefOf.Cancel.KeyDownEvent) && !GUI.GetNameOfFocusedControl().NullOrEmpty()) + { + Event.current.Use(); + UI.UnfocusCurrentControl(); + Find.WindowStack.Notify_ManuallySetFocus(window); + } + } + + public static void LabelWithTip(Rect rect, string str, string tip) + { + Widgets.Label(rect, str); + + if (Mouse.IsOver(rect) && tip != null) + TooltipHandler.TipRegion(rect, new TipSignal(tip, 3333)); + } + + public static void LabelTruncatedWithTip(Rect rect, string str, Dictionary cache) + { + var trunc = str.Truncate(rect.width, cache); + LabelWithTip(rect, trunc, trunc != str ? str : null); + } + + public static void CheckboxLabeledWithTip(Rect rect, string label, string tip, ref bool check, bool disabled = false) + { + Widgets.DrawHighlightIfMouseover(rect); + if (tip != null) + TooltipHandler.TipRegion(rect, tip); + CheckboxLabeled(rect, label, ref check, disabled); + } + + public static void CheckboxLabeledWithTipNoHighlight(Rect rect, string label, string tip, ref bool check, bool disabled = false, bool placeTextNearCheckbox = false) + { + if (tip != null) + TooltipHandler.TipRegion(rect, tip); + CheckboxLabeled(rect, label, ref check, disabled, order: placeTextNearCheckbox ? ElementOrder.Right : ElementOrder.Apart); + } + + public static bool ButtonTextWithTip(Rect rect, string label, string tip, bool disabled = false, int? tipId = null) + { + if (tip != null) + TooltipHandler.TipRegion(rect, new TipSignal(tip, tipId ?? tip.GetHashCode())); + + using (MpStyle.Set(TextAnchor.MiddleCenter)) + using (MpStyle.Set(disabled ? Color.grey : Color.white)) + { + var atlas = !disabled && Mouse.IsOver(rect) ? + (Input.GetMouseButton(0) ? Widgets.ButtonBGAtlasClick : Widgets.ButtonBGAtlasMouseover) : + Widgets.ButtonBGAtlas; + + Widgets.DrawAtlas(rect, atlas); + Widgets.Label(rect, label); + } + + var clicked = Widgets.ButtonInvisible(rect, false); + + if (!disabled) + { + MouseoverSounds.DoRegion(rect); + return clicked; + } + + if (clicked) + SoundDefOf.ClickReject.PlayOneShotOnCamera(); + + return false; + } + + public static float LabelFlexibleWidth(Rect rect, string label) + { + var size = Text.CalcSize(label); + rect.width = size.x; + Widgets.Label(rect, label); + return size.x; + } + + public static float LabelFlexibleWidthClickable(Rect rect, string label, ref bool clicked) + { + var size = Text.CalcSize(label); + rect.width = size.x; + Widgets.DrawHighlightIfMouseover(rect); + clicked = Widgets.ButtonInvisible(rect, false); + Widgets.Label(rect, label); + return size.x; + } + + const char PassChar = '\u2022'; + + public static void DoPasswordField(Rect rect, string controlName, ref string password) + { + password ??= ""; + + TextEditor te = + GUI.GetNameOfFocusedControl() == controlName ? + (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl) : + null; + + Vector2 scrollOffset = default; + bool keyDown = Event.current.type == EventType.KeyDown; + if (te != null) + scrollOffset = te.scrollOffset; + + GUI.SetNextControlName(controlName); + + if (Event.current.type is EventType.Repaint or EventType.MouseDown) + GUI.TextField( + rect, + new string(PassChar, password.Length), + Text.textFieldStyles[1] + ); + else + password = GUI.TextField( + rect, + password, + Text.textFieldStyles[1] + ); + + if (te != null && keyDown) + { + te.scrollOffset = scrollOffset; te.text = new string(PassChar, password.Length); +#if !RIMWORLD_1_7 AccessTools.Field(typeof(TextEditor), "m_RevealCursor").SetValue(te, true); te.UpdateScrollOffsetIfNeeded(new Event()); - } - } - } - - public enum ElementOrder - { - Apart, Left, Right - } -} +#endif + } + } + } + + public enum ElementOrder + { + Apart, Left, Right + } +} diff --git a/Source/Client/Util/MpUtil.cs b/Source/Client/Util/MpUtil.cs index 13264989b..ae7ee5f4b 100644 --- a/Source/Client/Util/MpUtil.cs +++ b/Source/Client/Util/MpUtil.cs @@ -1,203 +1,206 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Serialization; -using System.Text; -using HarmonyLib; -using Verse; - -namespace Multiplayer.Client -{ - public static class MpUtil - { +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text; +using HarmonyLib; +using Verse; + +namespace Multiplayer.Client +{ + public static class MpUtil + { public static unsafe void MarkNoInlining(MethodBase method) { + // This offset is a MonoMethod layout, not a CoreCLR MethodDesc. + // Harmony handles detouring on CoreCLR; writing here corrupts memory. + if (!Native.IsMonoRuntime) return; ushort* iflags = (ushort*)(method.MethodHandle.Value) + 1; - *iflags |= (ushort)MethodImplOptions.NoInlining; - } - - public static object NewObjectNoCtor(Type type) - { - return FormatterServices.GetUninitializedObject(type); - } - - public static T NewObjectNoCtor() - { - return (T)NewObjectNoCtor(typeof(T)); - } - - private static bool ipv6; - private static bool ipv6Checked; - - // Socket.OSSupportsIPv6 doesn't seem to work - public static bool SupportsIPv6() - { - if (!ipv6Checked) - { - try - { - new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - ipv6 = true; - } - catch - { - ipv6 = false; - } - - ipv6Checked = true; - } - - return ipv6; - } - - public static string RwDataFile(string filename) - { - return Path.Combine(GenFilePaths.SaveDataFolderPath, filename); - } - - public static To ShallowCopy(From from, To to) where To : From - { - foreach (var f in AccessTools.GetDeclaredFields(typeof(From))) - if (!f.IsStatic) - f.SetValue(to, f.GetValue(from)); - - return to; - } - - public static string TranslateWithDoubleNewLines(string keyBase, int count) - { - return Enumerable.Range(1, count).Select(n => (keyBase + n).Translate()).Join(delimiter: "\n\n"); - } - - public static string DelegateMethodInfo(MethodBase m) - { - return - m == null - ? "No method" - : $"{m.DeclaringType.DeclaringType?.FullDescription()} {m.DeclaringType.FullDescription()} {m.Name}" - .Replace("<", "[").Replace(">", "]"); - } - - public static MethodBase GetOriginalFromHarmonyReplacement(long replacementAddr) - { - // Todo: this is using a non-public API of Harmony, we should refactor to use https://harmony.pardeike.net/api/HarmonyLib.Harmony.html#HarmonyLib_Harmony_GetOriginalMethod_System_Reflection_MethodInfo_ - // as pardeike suggested in https://github.com/rwmt/Multiplayer/pull/270#issuecomment-1003298289 - return HarmonySharedState.originals - .FirstOrDefault(kv => kv.Key.MethodHandle.GetFunctionPointer().ToInt64() == replacementAddr).Value; - } - - public static string JoinStringsAtMost(this IEnumerable strs, int atMost = 3) - { - return strs.Take(atMost).Join() + (strs.Count() > atMost ? "..." : ""); - } - } - - public struct Container - { - public T Inner { get; } - - public Container(T value) - { - Inner = value; - } - - public static implicit operator Container(T value) - { - return new Container(value); - } - } - - public class UniqueList : IEnumerable - { - private List list = new List(); - private HashSet set = new HashSet(); - - public int Count => list.Count; - public T this[int index] => list[index]; - - public bool Add(T t) - { - if (set.Add(t)) - { - list.Add(t); - return true; - } - - return false; - } - - public T[] ToArray() - { - return list.ToArray(); - } - - public bool Contains(T t) - { - return set.Contains(t); - } - - public int IndexOf(T t) - { - return list.IndexOf(t); - } - - public IEnumerator GetEnumerator() - { - return list.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return list.GetEnumerator(); - } - } - - public class IdentityComparer : IEqualityComparer - { - public static IdentityComparer Instance = new(); - - public bool Equals(T x, T y) - { - return object.Equals(x, y); - } - - public int GetHashCode(T obj) - { - return RuntimeHelpers.GetHashCode(obj); - } - } - - public sealed class Utf8StringWriter : StringWriter - { - public override Encoding Encoding => Encoding.UTF8; - } - - public class FixedSizeQueue : IEnumerable - { - private Queue q = new Queue(); - - public int Limit { get; set; } - - public void Enqueue(T obj) - { - q.Enqueue(obj); - while (q.Count > Limit) q.Dequeue(); - } - - public IEnumerator GetEnumerator() - { - return q.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return q.GetEnumerator(); - } - } -} + *iflags |= (ushort)MethodImplOptions.NoInlining; + } + + public static object NewObjectNoCtor(Type type) + { + return FormatterServices.GetUninitializedObject(type); + } + + public static T NewObjectNoCtor() + { + return (T)NewObjectNoCtor(typeof(T)); + } + + private static bool ipv6; + private static bool ipv6Checked; + + // Socket.OSSupportsIPv6 doesn't seem to work + public static bool SupportsIPv6() + { + if (!ipv6Checked) + { + try + { + new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + ipv6 = true; + } + catch + { + ipv6 = false; + } + + ipv6Checked = true; + } + + return ipv6; + } + + public static string RwDataFile(string filename) + { + return Path.Combine(GenFilePaths.SaveDataFolderPath, filename); + } + + public static To ShallowCopy(From from, To to) where To : From + { + foreach (var f in AccessTools.GetDeclaredFields(typeof(From))) + if (!f.IsStatic) + f.SetValue(to, f.GetValue(from)); + + return to; + } + + public static string TranslateWithDoubleNewLines(string keyBase, int count) + { + return Enumerable.Range(1, count).Select(n => (keyBase + n).Translate()).Join(delimiter: "\n\n"); + } + + public static string DelegateMethodInfo(MethodBase m) + { + return + m == null + ? "No method" + : $"{m.DeclaringType.DeclaringType?.FullDescription()} {m.DeclaringType.FullDescription()} {m.Name}" + .Replace("<", "[").Replace(">", "]"); + } + + public static MethodBase GetOriginalFromHarmonyReplacement(long replacementAddr) + { + // Todo: this is using a non-public API of Harmony, we should refactor to use https://harmony.pardeike.net/api/HarmonyLib.Harmony.html#HarmonyLib_Harmony_GetOriginalMethod_System_Reflection_MethodInfo_ + // as pardeike suggested in https://github.com/rwmt/Multiplayer/pull/270#issuecomment-1003298289 + return HarmonySharedState.originals + .FirstOrDefault(kv => kv.Key.MethodHandle.GetFunctionPointer().ToInt64() == replacementAddr).Value; + } + + public static string JoinStringsAtMost(this IEnumerable strs, int atMost = 3) + { + return strs.Take(atMost).Join() + (strs.Count() > atMost ? "..." : ""); + } + } + + public struct Container + { + public T Inner { get; } + + public Container(T value) + { + Inner = value; + } + + public static implicit operator Container(T value) + { + return new Container(value); + } + } + + public class UniqueList : IEnumerable + { + private List list = new List(); + private HashSet set = new HashSet(); + + public int Count => list.Count; + public T this[int index] => list[index]; + + public bool Add(T t) + { + if (set.Add(t)) + { + list.Add(t); + return true; + } + + return false; + } + + public T[] ToArray() + { + return list.ToArray(); + } + + public bool Contains(T t) + { + return set.Contains(t); + } + + public int IndexOf(T t) + { + return list.IndexOf(t); + } + + public IEnumerator GetEnumerator() + { + return list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return list.GetEnumerator(); + } + } + + public class IdentityComparer : IEqualityComparer + { + public static IdentityComparer Instance = new(); + + public bool Equals(T x, T y) + { + return object.Equals(x, y); + } + + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } + + public sealed class Utf8StringWriter : StringWriter + { + public override Encoding Encoding => Encoding.UTF8; + } + + public class FixedSizeQueue : IEnumerable + { + private Queue q = new Queue(); + + public int Limit { get; set; } + + public void Enqueue(T obj) + { + q.Enqueue(obj); + while (q.Count > Limit) q.Dequeue(); + } + + public IEnumerator GetEnumerator() + { + return q.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return q.GetEnumerator(); + } + } +} diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 9e94cb170..2f20117b2 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -1,42 +1,43 @@ - - - - net48 - true - enable - 12 - false - false - Multiplayer.Common - MultiplayerCommon - - - - - - - - - - - - - - - - - - - - - - - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - - - - + + + + net48 + net10.0 + true + enable + 12 + false + false + Multiplayer.Common + MultiplayerCommon + + + + + + + + + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + + + + diff --git a/Source/Common/Native.cs b/Source/Common/Native.cs index 12abbd423..8b64b7b52 100644 --- a/Source/Common/Native.cs +++ b/Source/Common/Native.cs @@ -1,269 +1,275 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using HarmonyLib; - -namespace Multiplayer.Client -{ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using HarmonyLib; + +namespace Multiplayer.Client +{ public static class Native { - public enum NativeOS - { - Windows, OSX, Linux, Dummy - } - - public static IntPtr DomainPtr { get; private set; } - - // LMF is Last Managed Frame - // current_thread->internal_thread->thread_info->tls[4] - public static long LmfPtr { get; private set; } - - public static Func? HarmonyOriginalGetter { get; set; } - + // Waylith uses CoreCLR. Never load Mono libraries or dereference Mono + // thread structures on that runtime, even when running old mod assemblies. + public static bool IsMonoRuntime => Type.GetType("Mono.Runtime") != null; + + public enum NativeOS + { + Windows, OSX, Linux, Dummy + } + + public static IntPtr DomainPtr { get; private set; } + + // LMF is Last Managed Frame + // current_thread->internal_thread->thread_info->tls[4] + public static long LmfPtr { get; private set; } + + public static Func? HarmonyOriginalGetter { get; set; } + public static void EarlyInit(NativeOS os) { - if (os == NativeOS.Linux) - TheLinuxWay(); - if (os == NativeOS.OSX) - TheOSXWay(); - - EarlyInitInternal(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void TheLinuxWay() => mono_dllmap_insert_linux(IntPtr.Zero, MonoWindows, null, MonoLinux, null); - - [MethodImpl(MethodImplOptions.NoInlining)] - static void TheOSXWay() => mono_dllmap_insert_osx(IntPtr.Zero, MonoWindows, null, MonoOSX, null); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void EarlyInitInternal() - { - DomainPtr = mono_domain_get(); - } - + if (!IsMonoRuntime) return; + if (os == NativeOS.Linux) + TheLinuxWay(); + if (os == NativeOS.OSX) + TheOSXWay(); + + EarlyInitInternal(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TheLinuxWay() => mono_dllmap_insert_linux(IntPtr.Zero, MonoWindows, null, MonoLinux, null); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TheOSXWay() => mono_dllmap_insert_osx(IntPtr.Zero, MonoWindows, null, MonoOSX, null); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void EarlyInitInternal() + { + DomainPtr = mono_domain_get(); + } + public static unsafe void InitLmfPtr(NativeOS os) { - // Don't bother on 32 bit runtimes - if (IntPtr.Size == 4) - return; - - const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | - BindingFlags.NonPublic; - - var internalThreadField = typeof(Thread).GetField("internal_thread", all); - var threadInfoField = internalThreadField.FieldType.GetField("runtime_thread_info", all); - var threadInfoPtr = (long)(IntPtr)threadInfoField.GetValue(internalThreadField.GetValue(Thread.CurrentThread)); - - // Struct offset found manually - // Navigate by string: "Handle Stack" - // Updated for RimWorld 1.6 (Unity 2022.3.35f1, Mono 6.13.0) - if (os == NativeOS.Linux) - LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) - else if (os == NativeOS.Windows) - LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. - else if (os == NativeOS.OSX) - LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) - else if (os == NativeOS.Dummy) - { - LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); - *(long*)LmfPtr = LmfPtr; - *(long*)(LmfPtr + 8) = 0; - } - } - - private static IntPtr MaybeFindHarmonyOriginalMethod(long addr, bool harmonyOriginals) - { - var ji = mono_jit_info_table_find(DomainPtr, (IntPtr)addr); - if (ji == IntPtr.Zero) return IntPtr.Zero; - - var ptr = mono_jit_info_get_method(ji); - var codeStart = (long)mono_jit_info_get_code_start(ji); - - if (harmonyOriginals && HarmonyOriginalGetter != null) - { - var original = HarmonyOriginalGetter(codeStart); - if (original != null) - ptr = original.MethodHandle.Value; - } - - return ptr; - } - - public static string? MethodNameFromAddr(long addr, bool harmonyOriginals) - { - var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); - if (ptr == IntPtr.Zero) return null; - var name = mono_debug_print_stack_frame(ptr, -1, DomainPtr); - return string.IsNullOrEmpty(name) ? null : name; - } - - public static string? MethodNameNormalizedFromAddr(long addr, bool harmonyOriginals) - { - var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); - return ptr == IntPtr.Zero ? null : mono_method_get_reflection_name(ptr); - } - - private static ConstructorInfo runtimeMethodHandleCtor = AccessTools.Constructor(typeof(RuntimeMethodHandle), [typeof(IntPtr)]); - - public static bool GetMethodAggressiveInlining(long addr) - { - var domain = DomainPtr; - var ji = mono_jit_info_table_find(domain, (IntPtr)addr); - - if (ji == IntPtr.Zero) return false; - - var methodHandle = mono_jit_info_get_method(ji); - var methodBase = GetMethodBaseFromRuntimePointer(methodHandle); - - if (methodBase == null) return false; - - return (methodBase.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0; - } - - public static MethodBase? GetMethodBaseFromRuntimePointer(IntPtr methodHandle) - { - if (methodHandle == IntPtr.Zero) - return null; - var rmh = (RuntimeMethodHandle)runtimeMethodHandleCtor.Invoke([methodHandle]); - var rth = new RuntimeTypeHandle(); - var methodBase = MethodBase.GetMethodFromHandle(rmh, rth); - return methodBase; - } - - // const string MonoWindows = "mono-2.0-sgen.dll"; - const string MonoWindows = "mono-2.0-bdwgc"; - const string MonoLinux = "libmonobdwgc-2.0.so"; - const string MonoOSX = "libmonobdwgc-2.0.dylib"; - - [DllImport(MonoLinux, EntryPoint = "mono_dllmap_insert")] - private static extern void mono_dllmap_insert_linux(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); - - [DllImport(MonoOSX, EntryPoint = "mono_dllmap_insert")] - private static extern void mono_dllmap_insert_osx(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_table_find(IntPtr domain, IntPtr addr); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_get_method(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_get_code_start(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern int mono_jit_info_get_code_size(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern string mono_debug_print_stack_frame(IntPtr method, int nativeOffset, IntPtr domain); - - [DllImport(MonoWindows)] - public static extern int mini_parse_debug_option(string option); - - [DllImport(MonoWindows)] - public static extern void mono_set_defaults(int verboseLevel, int opts); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_domain_get(); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_compile_method(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_from_mono_type(IntPtr type); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_vtable(IntPtr domain, IntPtr klass); - - [DllImport(MonoWindows)] - public static extern int mono_method_get_token(IntPtr method); - - [DllImport(MonoWindows)] - public static extern string mono_method_get_reflection_name(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_method_get_class(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_get_image(IntPtr klass); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_profiler_create(IntPtr profiler); - - public delegate void JitDoneCallback(IntPtr profiler, IntPtr method, IntPtr jinfo); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_profiler_set_jit_done_callback(IntPtr profiler, JitDoneCallback? cb); - - public static unsafe bool CctorRan(Type t) - { - return *((byte*)mono_class_vtable(mono_domain_get(), mono_class_from_mono_type(t.TypeHandle.Value)) + 45) != 0; - } - } - - /*public static class IcedDisasm - { - public unsafe static void DisasmMethod(long codeStart) - { - DisasmMethod(codeStart, Native.mono_jit_info_get_code_size(Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)codeStart))); - } - - public unsafe static void DisasmMethod(long codeStart, long codeSize) - { - //Log.Message($"code {Convert.ToString(*(uint*)codeStart, 16)}"); - //return; - - var codeRip = (ulong)codeStart; - var codeReader = new UnsafeCodeReader() { start = (byte*)codeStart }; - var decoder = Decoder.Create(64, codeReader); - decoder.IP = codeRip; - ulong endRip = decoder.IP + (uint)codeSize + 8; - - var instructions = new InstructionList(); - while (decoder.IP < endRip) - decoder.Decode(out instructions.AllocUninitializedElement()); - - var formatter = new NasmFormatter(); - formatter.Options.DigitSeparator = "`"; - formatter.Options.FirstOperandCharIndex = 10; - var output = new StringOutput(); - - bool cond = (*(uint*)codeStart & 0xFFFFFF) != 0xEC8348 && *(byte*)codeStart != 0x55; - - Log.Message("Code size: " + codeSize, true); - - foreach (ref var instr in instructions) - { - var stringb = new StringBuilder(); - formatter.Format(instr, output); - stringb.Append(instr.IP.ToString("X16")); - stringb.Append(" "); - int instrLen = instr.Length; - int byteBaseIndex = (int)(instr.IP - codeRip); - for (int i = 0; i < instrLen; i++) - stringb.Append(((byte*)codeStart)[byteBaseIndex + i].ToString("X2")); - int missingBytes = 10 - instrLen; - for (int i = 0; i < missingBytes; i++) - stringb.Append(" "); - stringb.Append(" "); - stringb.Append(output.ToStringAndReset()); - //if (cond) - Log.Message(stringb.ToString(), ignoreStopLoggingLimit: true); - } - } - - public unsafe class UnsafeCodeReader : CodeReader - { - public byte* start; - - public unsafe override int ReadByte() - { - var val = *start; - start++; - return val; - } - } - }*/ -} + if (!IsMonoRuntime) return; + // Don't bother on 32 bit runtimes + if (IntPtr.Size == 4) + return; + + const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic; + + var internalThreadField = typeof(Thread).GetField("internal_thread", all); + var threadInfoField = internalThreadField.FieldType.GetField("runtime_thread_info", all); + var threadInfoPtr = (long)(IntPtr)threadInfoField.GetValue(internalThreadField.GetValue(Thread.CurrentThread)); + + // Struct offset found manually + // Navigate by string: "Handle Stack" + // Updated for RimWorld 1.6 (Unity 2022.3.35f1, Mono 6.13.0) + if (os == NativeOS.Linux) + LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) + else if (os == NativeOS.Windows) + LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. + else if (os == NativeOS.OSX) + LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) + else if (os == NativeOS.Dummy) + { + LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); + *(long*)LmfPtr = LmfPtr; + *(long*)(LmfPtr + 8) = 0; + } + } + + private static IntPtr MaybeFindHarmonyOriginalMethod(long addr, bool harmonyOriginals) + { + var ji = mono_jit_info_table_find(DomainPtr, (IntPtr)addr); + if (ji == IntPtr.Zero) return IntPtr.Zero; + + var ptr = mono_jit_info_get_method(ji); + var codeStart = (long)mono_jit_info_get_code_start(ji); + + if (harmonyOriginals && HarmonyOriginalGetter != null) + { + var original = HarmonyOriginalGetter(codeStart); + if (original != null) + ptr = original.MethodHandle.Value; + } + + return ptr; + } + + public static string? MethodNameFromAddr(long addr, bool harmonyOriginals) + { + var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); + if (ptr == IntPtr.Zero) return null; + var name = mono_debug_print_stack_frame(ptr, -1, DomainPtr); + return string.IsNullOrEmpty(name) ? null : name; + } + + public static string? MethodNameNormalizedFromAddr(long addr, bool harmonyOriginals) + { + var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); + return ptr == IntPtr.Zero ? null : mono_method_get_reflection_name(ptr); + } + + private static ConstructorInfo runtimeMethodHandleCtor = AccessTools.Constructor(typeof(RuntimeMethodHandle), [typeof(IntPtr)]); + + public static bool GetMethodAggressiveInlining(long addr) + { + var domain = DomainPtr; + var ji = mono_jit_info_table_find(domain, (IntPtr)addr); + + if (ji == IntPtr.Zero) return false; + + var methodHandle = mono_jit_info_get_method(ji); + var methodBase = GetMethodBaseFromRuntimePointer(methodHandle); + + if (methodBase == null) return false; + + return (methodBase.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0; + } + + public static MethodBase? GetMethodBaseFromRuntimePointer(IntPtr methodHandle) + { + if (methodHandle == IntPtr.Zero) + return null; + var rmh = (RuntimeMethodHandle)runtimeMethodHandleCtor.Invoke([methodHandle]); + var rth = new RuntimeTypeHandle(); + var methodBase = MethodBase.GetMethodFromHandle(rmh, rth); + return methodBase; + } + + // const string MonoWindows = "mono-2.0-sgen.dll"; + const string MonoWindows = "mono-2.0-bdwgc"; + const string MonoLinux = "libmonobdwgc-2.0.so"; + const string MonoOSX = "libmonobdwgc-2.0.dylib"; + + [DllImport(MonoLinux, EntryPoint = "mono_dllmap_insert")] + private static extern void mono_dllmap_insert_linux(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); + + [DllImport(MonoOSX, EntryPoint = "mono_dllmap_insert")] + private static extern void mono_dllmap_insert_osx(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_table_find(IntPtr domain, IntPtr addr); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_get_method(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_get_code_start(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern int mono_jit_info_get_code_size(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern string mono_debug_print_stack_frame(IntPtr method, int nativeOffset, IntPtr domain); + + [DllImport(MonoWindows)] + public static extern int mini_parse_debug_option(string option); + + [DllImport(MonoWindows)] + public static extern void mono_set_defaults(int verboseLevel, int opts); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_domain_get(); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_compile_method(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_from_mono_type(IntPtr type); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_vtable(IntPtr domain, IntPtr klass); + + [DllImport(MonoWindows)] + public static extern int mono_method_get_token(IntPtr method); + + [DllImport(MonoWindows)] + public static extern string mono_method_get_reflection_name(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_method_get_class(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_get_image(IntPtr klass); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_profiler_create(IntPtr profiler); + + public delegate void JitDoneCallback(IntPtr profiler, IntPtr method, IntPtr jinfo); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_profiler_set_jit_done_callback(IntPtr profiler, JitDoneCallback? cb); + + public static unsafe bool CctorRan(Type t) + { + return *((byte*)mono_class_vtable(mono_domain_get(), mono_class_from_mono_type(t.TypeHandle.Value)) + 45) != 0; + } + } + + /*public static class IcedDisasm + { + public unsafe static void DisasmMethod(long codeStart) + { + DisasmMethod(codeStart, Native.mono_jit_info_get_code_size(Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)codeStart))); + } + + public unsafe static void DisasmMethod(long codeStart, long codeSize) + { + //Log.Message($"code {Convert.ToString(*(uint*)codeStart, 16)}"); + //return; + + var codeRip = (ulong)codeStart; + var codeReader = new UnsafeCodeReader() { start = (byte*)codeStart }; + var decoder = Decoder.Create(64, codeReader); + decoder.IP = codeRip; + ulong endRip = decoder.IP + (uint)codeSize + 8; + + var instructions = new InstructionList(); + while (decoder.IP < endRip) + decoder.Decode(out instructions.AllocUninitializedElement()); + + var formatter = new NasmFormatter(); + formatter.Options.DigitSeparator = "`"; + formatter.Options.FirstOperandCharIndex = 10; + var output = new StringOutput(); + + bool cond = (*(uint*)codeStart & 0xFFFFFF) != 0xEC8348 && *(byte*)codeStart != 0x55; + + Log.Message("Code size: " + codeSize, true); + + foreach (ref var instr in instructions) + { + var stringb = new StringBuilder(); + formatter.Format(instr, output); + stringb.Append(instr.IP.ToString("X16")); + stringb.Append(" "); + int instrLen = instr.Length; + int byteBaseIndex = (int)(instr.IP - codeRip); + for (int i = 0; i < instrLen; i++) + stringb.Append(((byte*)codeStart)[byteBaseIndex + i].ToString("X2")); + int missingBytes = 10 - instrLen; + for (int i = 0; i < missingBytes; i++) + stringb.Append(" "); + stringb.Append(" "); + stringb.Append(output.ToStringAndReset()); + //if (cond) + Log.Message(stringb.ToString(), ignoreStopLoggingLimit: true); + } + } + + public unsafe class UnsafeCodeReader : CodeReader + { + public byte* start; + + public unsafe override int ReadByte() + { + var val = *start; + start++; + return val; + } + } + }*/ +} diff --git a/Source/Directory.Build.targets b/Source/Directory.Build.targets new file mode 100644 index 000000000..91306404b --- /dev/null +++ b/Source/Directory.Build.targets @@ -0,0 +1,26 @@ + + + + net10.0 + $(DefineConstants);RIMWORLD_1_7 + true + + + + + + + + false + + + + + + + + + + + + diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index 018083c71..cd008eaea 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -1,37 +1,38 @@ - - - - net48 - true - 12 - false - false - MultiplayerLoader - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - + + + + net48 + net10.0 + true + 12 + false + false + MultiplayerLoader + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + diff --git a/Source/MultiplayerLoader/Prepatches.cs b/Source/MultiplayerLoader/Prepatches.cs index 3261b5497..a42334c42 100644 --- a/Source/MultiplayerLoader/Prepatches.cs +++ b/Source/MultiplayerLoader/Prepatches.cs @@ -1,83 +1,85 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using MonoMod.Utils; -using Multiplayer.Common; -using Prepatcher; -using RimWorld; - -namespace MultiplayerLoader; - -public static class Prepatches -{ - [FreePatch] - static void DontSetResolution(ModuleDefinition module) - { - if (MpVersion.IsDebug) - { - var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); - resolutionUtilityType.FindMethod(nameof(ResolutionUtility.SetResolutionRaw))!.Body.Instructions.Insert( - 0, - Instruction.Create(OpCodes.Ret) - ); - } - } - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; +using Multiplayer.Common; +using Prepatcher; +using RimWorld; + +namespace MultiplayerLoader; + +public static class Prepatches +{ + [FreePatch] + static void DontSetResolution(ModuleDefinition module) + { + if (MpVersion.IsDebug) + { + var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); + resolutionUtilityType.FindMethod(nameof(ResolutionUtility.SetResolutionRaw))!.Body.Instructions.Insert( + 0, + Instruction.Create(OpCodes.Ret) + ); + } + } + [FreePatch] static void ApplySyncPrepatches(ModuleDefinition module) { +#if !RIMWORLD_1_7 SyncRituals.ApplyPrepatches(module); - } - - internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix) - { - var targetMethod = module.ImportReference(target).Resolve(); - var firstInst = targetMethod.Body.Instructions.First(); - - IEnumerable PrefixInstructions() - { - yield return Instruction.Create(OpCodes.Ldc_I4, id); // Load index - yield return Instruction.Create(OpCodes.Ldarg_0); // Load __instance - - // Create __args array - yield return Instruction.Create(OpCodes.Ldc_I4, target.GetParameters().Length); - yield return Instruction.Create(OpCodes.Newarr, module.TypeSystem.Object); - - int idx = 0; - foreach (var pInfo in target.GetParameters()) - { - var pType = pInfo.ParameterType; - - yield return Instruction.Create(OpCodes.Dup); - yield return Instruction.Create(OpCodes.Ldc_I4, idx); - yield return Instruction.Create(OpCodes.Ldarg, targetMethod.Parameters.ElementAt(idx)); - - if (pType.IsByRef) - throw new Exception("Byrefs aren't handled"); - if (pType.IsValueType) - yield return Instruction.Create(OpCodes.Box, module.ImportReference(pType)); - - yield return Instruction.Create(OpCodes.Stelem_Ref); - - idx++; - } - - yield return Instruction.Create(OpCodes.Call, module.ImportReference(prefix)); - yield return Instruction.Create(OpCodes.Brtrue, firstInst); - - if (target.ReturnType == typeof(bool)) - yield return Instruction.Create(OpCodes.Ldc_I4_0); - else if (target.ReturnType == typeof(void)) - yield return Instruction.Create(OpCodes.Nop); - else - throw new Exception($"Can't handle return type {target.ReturnType}"); - - yield return Instruction.Create(OpCodes.Ret); - } - - targetMethod.Body.Instructions.InsertRange(0, PrefixInstructions()); - } -} +#endif + } + + internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix) + { + var targetMethod = module.ImportReference(target).Resolve(); + var firstInst = targetMethod.Body.Instructions.First(); + + IEnumerable PrefixInstructions() + { + yield return Instruction.Create(OpCodes.Ldc_I4, id); // Load index + yield return Instruction.Create(OpCodes.Ldarg_0); // Load __instance + + // Create __args array + yield return Instruction.Create(OpCodes.Ldc_I4, target.GetParameters().Length); + yield return Instruction.Create(OpCodes.Newarr, module.TypeSystem.Object); + + int idx = 0; + foreach (var pInfo in target.GetParameters()) + { + var pType = pInfo.ParameterType; + + yield return Instruction.Create(OpCodes.Dup); + yield return Instruction.Create(OpCodes.Ldc_I4, idx); + yield return Instruction.Create(OpCodes.Ldarg, targetMethod.Parameters.ElementAt(idx)); + + if (pType.IsByRef) + throw new Exception("Byrefs aren't handled"); + if (pType.IsValueType) + yield return Instruction.Create(OpCodes.Box, module.ImportReference(pType)); + + yield return Instruction.Create(OpCodes.Stelem_Ref); + + idx++; + } + + yield return Instruction.Create(OpCodes.Call, module.ImportReference(prefix)); + yield return Instruction.Create(OpCodes.Brtrue, firstInst); + + if (target.ReturnType == typeof(bool)) + yield return Instruction.Create(OpCodes.Ldc_I4_0); + else if (target.ReturnType == typeof(void)) + yield return Instruction.Create(OpCodes.Nop); + else + throw new Exception($"Can't handle return type {target.ReturnType}"); + + yield return Instruction.Create(OpCodes.Ret); + } + + targetMethod.Body.Instructions.InsertRange(0, PrefixInstructions()); + } +} diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs index 24a1c4af6..cb845dab1 100644 --- a/Source/MultiplayerLoader/SyncRituals.cs +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -1,135 +1,153 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using Mono.Cecil; -using Multiplayer.API; -using Multiplayer.Client.Util; -using RimWorld; - -namespace MultiplayerLoader; - -public static class SyncRituals -{ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Mono.Cecil; +using Multiplayer.API; +using Multiplayer.Client.Util; +using RimWorld; + +namespace MultiplayerLoader; + +public static class SyncRituals +{ private static List syncMethodsWithPrepatches = new(); +#if RIMWORLD_1_7 + private static readonly Dictionary<(Type, string), ISyncMethodForPrepatcher> runtimeSyncMethods = new(); + private static readonly HashSet runtimePatchedMethods = new(); + private static readonly Harmony runtimeHarmony = new("rwmt.Multiplayer.WaylithRituals"); - public static bool PrepatcherPrefix(int index, object __instance, object[] __args) + public static bool RuntimePrefix(object __instance, object[] __args, MethodBase __originalMethod) { - // This prefix is applied to a generic method and called by all possible instantiations - // This check makes sure the type is correct - if (syncMethodsWithPrepatches[index].TargetType == __instance.GetType()) - return !syncMethodsWithPrepatches[index].DoSync(__instance, __args); - return true; + return !runtimeSyncMethods.TryGetValue((__instance.GetType(), __originalMethod.Name), out var sync) + || !sync.DoSync(__instance, __args); } - - private static MethodInfo syncMethod = AccessTools.Method("Multiplayer.Client.Sync:Method"); - - // This method is called twice (two passes) - // The first pass with module != null is called from Prepatcher and applies prepatches - // The second pass with module == null is called from Sync initialization and registers SyncMethods - public static void ApplyPrepatches(ModuleDefinition module) - { - void Register(Type baseType, string method, Type derivedType, Action postProcess = null) - { - if (module != null) - { - Prepatches.AddPrepatcherPrefix( - module, - syncMethodsWithPrepatches.Count, - AccessTools.Method(baseType, method), - AccessTools.Method(typeof(SyncRituals), nameof(PrepatcherPrefix)) - ); - - // The ids need to be in sync between passes (note the array is recreated after Prepatcher restarts the game) - syncMethodsWithPrepatches.Add(null); - } - else - { - // Sync.Method doesn't apply a Harmony patch and doesn't require the DeclaringType to be the target type - var newSyncMethod = (ISyncMethodForPrepatcher)syncMethod.Invoke(null, [derivedType, method, null]); +#endif + + public static bool PrepatcherPrefix(int index, object __instance, object[] __args) + { + // This prefix is applied to a generic method and called by all possible instantiations + // This check makes sure the type is correct + if (syncMethodsWithPrepatches[index].TargetType == __instance.GetType()) + return !syncMethodsWithPrepatches[index].DoSync(__instance, __args); + return true; + } + + private static MethodInfo syncMethod = AccessTools.Method("Multiplayer.Client.Sync:Method"); + + // This method is called twice (two passes) + // The first pass with module != null is called from Prepatcher and applies prepatches + // The second pass with module == null is called from Sync initialization and registers SyncMethods + public static void ApplyPrepatches(ModuleDefinition module) + { + void Register(Type baseType, string method, Type derivedType, Action postProcess = null) + { + if (module != null) + { + Prepatches.AddPrepatcherPrefix( + module, + syncMethodsWithPrepatches.Count, + AccessTools.Method(baseType, method), + AccessTools.Method(typeof(SyncRituals), nameof(PrepatcherPrefix)) + ); + + // The ids need to be in sync between passes (note the array is recreated after Prepatcher restarts the game) + syncMethodsWithPrepatches.Add(null); + } + else + { + // Sync.Method doesn't apply a Harmony patch and doesn't require the DeclaringType to be the target type + var newSyncMethod = (ISyncMethodForPrepatcher)syncMethod.Invoke(null, [derivedType, method, null]); postProcess?.Invoke(newSyncMethod); syncMethodsWithPrepatches.Add(newSyncMethod); - } - } - - // ======= PawnRitualRoleSelectionWidget ======= - - var ritualRolesSerializer = Serializer.New, (PawnRitualRoleSelectionWidget roleSelectionWidget, string Key)>( - RitualRoleWriter, - RitualRoleReader - ); - var psychicRitualRolesSerializer = Serializer.New, (PawnPsychicRitualRoleSelectionWidget roleSelectionWidget, string Key)>( - RitualRoleWriter, - RitualRoleReader - ); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), - typeof(PawnRitualRoleSelectionWidget), - m => m.TransformArgument(1, ritualRolesSerializer)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), - typeof(PawnPsychicRitualRoleSelectionWidget), - m => m.TransformArgument(1, psychicRitualRolesSerializer)); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), - typeof(PawnRitualRoleSelectionWidget)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), - typeof(PawnPsychicRitualRoleSelectionWidget)); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), - typeof(PawnRitualRoleSelectionWidget), - m => m.TransformArgument(1, ritualRolesSerializer)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), - typeof(PawnPsychicRitualRoleSelectionWidget), - m => m.TransformArgument(1, psychicRitualRolesSerializer)); - - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 7).Name, - typeof(PawnRitualRoleSelectionWidget) - ); // Roles right click delegate (try assign spectate) - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 7).Name, - typeof(PawnPsychicRitualRoleSelectionWidget) - ); // Roles right click delegate (try assign spectate) - - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 2).Name, - typeof(PawnRitualRoleSelectionWidget) - ); // Not participating left click delegate (try assign any role or spectate) - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 2).Name, - typeof(PawnPsychicRitualRoleSelectionWidget) - ); // Not participating left click delegate (try assign any role or spectate) - } - - private static (WidgetType roleSelectionWidget, string Key) RitualRoleWriter(IEnumerable roles, object target, object[] _) - where WidgetType : PawnRoleSelectionWidgetBase - where RoleType : class, ILordJobRole - { - var roleSelectionWidget = (WidgetType)target; - return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); - } - - private static IEnumerable RitualRoleReader((WidgetType roleSelectionWidget, string Key) data) - where WidgetType : PawnRoleSelectionWidgetBase - where RoleType : class, ILordJobRole - { - return data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key); - } -} +#if RIMWORLD_1_7 + // Waylith supports runtime patching; the Mono preloader is not used. + runtimeSyncMethods.Add((derivedType, method), newSyncMethod); + var target = AccessTools.Method(derivedType, method); + if (runtimePatchedMethods.Add(target)) + runtimeHarmony.Patch(target, prefix: new HarmonyMethod(typeof(SyncRituals), nameof(RuntimePrefix))); +#endif + } + } + + // ======= PawnRitualRoleSelectionWidget ======= + + var ritualRolesSerializer = Serializer.New, (PawnRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader + ); + var psychicRitualRolesSerializer = Serializer.New, (PawnPsychicRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader + ); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnRitualRoleSelectionWidget)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnPsychicRitualRoleSelectionWidget)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + } + + private static (WidgetType roleSelectionWidget, string Key) RitualRoleWriter(IEnumerable roles, object target, object[] _) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + var roleSelectionWidget = (WidgetType)target; + return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); + } + + private static IEnumerable RitualRoleReader((WidgetType roleSelectionWidget, string Key) data) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + return data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key); + } +} diff --git a/Source/Tests/CoreClrRuntimeTest.cs b/Source/Tests/CoreClrRuntimeTest.cs new file mode 100644 index 000000000..d790e573f --- /dev/null +++ b/Source/Tests/CoreClrRuntimeTest.cs @@ -0,0 +1,17 @@ +using Multiplayer.Client; + +namespace Tests; + +public class CoreClrRuntimeTest +{ + [Test] + public void MonoInitializationIsSkippedOnCoreClr() + { + Assert.That(Native.IsMonoRuntime, Is.False); + // Both calls used to load Mono libraries or read Mono thread memory. + Native.EarlyInit(Native.NativeOS.Windows); + Native.InitLmfPtr(Native.NativeOS.Windows); + Assert.That(Native.DomainPtr, Is.EqualTo(IntPtr.Zero)); + Assert.That(Native.LmfPtr, Is.Zero); + } +} diff --git a/Source/Tests/Tests.csproj b/Source/Tests/Tests.csproj index 17b0b2fce..ecde80b82 100644 --- a/Source/Tests/Tests.csproj +++ b/Source/Tests/Tests.csproj @@ -1,32 +1,33 @@ - - - - net8.0 - enable - enable - 12 - - false - - - - - - - - - - - - - - - - - - - - - - - + + + + net8.0 + net10.0 + enable + enable + 12 + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt b/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt index b87199a17..08c775816 100644 --- a/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt +++ b/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt @@ -1,3 +1,3 @@ This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object -05-00-00-00-31-2E-30-2E-30-04-00-00-00-01-00-00-00-02-00-00-00-03-00-00-00-04-00-00-00-01-00-00-00-01-00-00-00-00-00-00-03-02-00-00-00-03-00-00-00-6B-65-79-01-00-00-00-7B-00-00-00-04-00-00-00-6B-65-79-32-00-00-00-00-00-00-00-00-00-24-00-00-00-63-64-60-60-10-00-E2-A2-F2-DC-12-BD-DC-D2-9C-92-CC-82-9C-C4-CA-D4-22-6E-A0-98-2F-82-0B-E4-81-01-33-98-04-00 (117 bytes) +05-00-00-00-31-2E-30-2E-30-04-00-00-00-01-00-00-00-02-00-00-00-03-00-00-00-04-00-00-00-01-00-00-00-01-00-00-00-00-00-00-03-02-00-00-00-03-00-00-00-6B-65-79-01-00-00-00-7B-00-00-00-04-00-00-00-6B-65-79-32-00-00-00-00-00-00-00-00-00-26-00-00-00-63-64-60-60-10-60-60-60-28-2A-CF-2D-D1-CB-2D-CD-29-C9-2C-C8-49-AC-4C-2D-E2-66-60-60-F0-45-70-19-A0-80-19-4C-01-00 (119 bytes) diff --git a/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt b/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt index 98e950920..0e11269af 100644 --- a/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt +++ b/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt @@ -1,3 +1,3 @@ This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object -08-00-00-00-47-61-6D-65-4E-61-6D-65-01-00-00-00-08-00-00-00-31-2E-36-2E-34-35-36-36-0D-00-00-00-30-2E-31-31-2E-30-2B-31-32-33-34-35-36-05-00-00-00-00-00-02-03-01-00-24-00-00-00-63-64-60-60-10-00-E2-A2-F2-DC-12-BD-DC-D2-9C-92-CC-82-9C-C4-CA-D4-22-6E-A0-98-2F-82-0B-E4-81-01-33-98-04-00 (95 bytes) +08-00-00-00-47-61-6D-65-4E-61-6D-65-01-00-00-00-08-00-00-00-31-2E-36-2E-34-35-36-36-0D-00-00-00-30-2E-31-31-2E-30-2B-31-32-33-34-35-36-05-00-00-00-00-00-02-03-01-00-26-00-00-00-63-64-60-60-10-60-60-60-28-2A-CF-2D-D1-CB-2D-CD-29-C9-2C-C8-49-AC-4C-2D-E2-66-60-60-F0-45-70-19-A0-80-19-4C-01-00 (97 bytes) From c3bfada92d8514dfda341d739c35743064509e8e Mon Sep 17 00:00:00 2001 From: mkiplm-svg Date: Mon, 19 Oct 2026 09:32:21 -0400 Subject: [PATCH 2/3] Normalize line endings in RimWorld 1.7 port --- Source/Client/Desyncs/DeferredStackTracing.cs | 278 +-- Source/Client/Desyncs/JittedMethods.cs | 210 +- Source/Client/Desyncs/SaveableDesyncInfo.cs | 396 ++-- Source/Client/Desyncs/StackTraceLogItem.cs | 170 +- Source/Client/Factions/MultifactionPatches.cs | 1918 ++++++++--------- Source/Client/Multiplayer.cs | 508 ++--- Source/Client/Multiplayer.csproj | 168 +- .../Patches/GravshipTravelSessionPatches.cs | 514 ++--- Source/Client/Patches/VanillaTweaks.cs | 294 +-- Source/Client/Syncing/SyncTemplates.cs | 176 +- Source/Client/UI/MainMenuAnimation.cs | 876 ++++---- Source/Client/Util/MpUI.cs | 546 ++--- Source/Client/Util/MpUtil.cs | 400 ++-- Source/Common/Common.csproj | 84 +- Source/Common/Native.cs | 526 ++--- .../MultiplayerLoader.csproj | 74 +- Source/MultiplayerLoader/Prepatches.cs | 158 +- Source/MultiplayerLoader/SyncRituals.cs | 264 +-- Source/Tests/Tests.csproj | 64 +- .../ClientInitDataPacket.verified.txt | 2 +- .../ServerJoinDataPacket.verified.txt | 2 +- 21 files changed, 3814 insertions(+), 3814 deletions(-) diff --git a/Source/Client/Desyncs/DeferredStackTracing.cs b/Source/Client/Desyncs/DeferredStackTracing.cs index f36c9395f..4b9ad7464 100644 --- a/Source/Client/Desyncs/DeferredStackTracing.cs +++ b/Source/Client/Desyncs/DeferredStackTracing.cs @@ -1,36 +1,36 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; -using System.Reflection; -using System.Runtime.InteropServices; -using HarmonyLib; -using Multiplayer.Client.Patches; -using Multiplayer.Common; -using RimWorld; -using Verse; -using Verse.AI; - -namespace Multiplayer.Client.Desyncs -{ - [EarlyPatch] - [HarmonyPatch] - public static class DeferredStackTracing - { - public static int ignoreTraces; - public static long maxTraceDepth; - public static int randCalls; - - static IEnumerable TargetMethods() - { - yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Value)); - yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Int)); - } - - public static int acc; - - private static bool SupportsDeferredStackTracing => - RuntimeInformation.ProcessArchitecture is Architecture.X64 or Architecture.X86; - +using System.Reflection; +using System.Runtime.InteropServices; +using HarmonyLib; +using Multiplayer.Client.Patches; +using Multiplayer.Common; +using RimWorld; +using Verse; +using Verse.AI; + +namespace Multiplayer.Client.Desyncs +{ + [EarlyPatch] + [HarmonyPatch] + public static class DeferredStackTracing + { + public static int ignoreTraces; + public static long maxTraceDepth; + public static int randCalls; + + static IEnumerable TargetMethods() + { + yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Value)); + yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Int)); + } + + public static int acc; + + private static bool SupportsDeferredStackTracing => + RuntimeInformation.ProcessArchitecture is Architecture.X64 or Architecture.X86; + public static void Postfix() { if (Native.IsMonoRuntime && Native.LmfPtr == 0) return; @@ -58,112 +58,112 @@ public static void Postfix() acc++; return; } - var trace = logItem.raw; - int hash = 0; - // Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value. - int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash, skipFrames: 1); - - Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); - - acc++; - } - - public static bool ShouldAddStackTraceForDesyncLog() - { - if (!SupportsDeferredStackTracing) return false; - if (Multiplayer.Client == null) return false; - if (Multiplayer.settings.desyncTracingMode == DesyncTracingMode.None) return false; - if (Multiplayer.game == null) return false; - - if (!Multiplayer.game.gameComp.logDesyncTraces) return false; - - if (Rand.stateStack.Count > 1) return false; - if (Multiplayer.IsReplay) return false; - - if (!Multiplayer.Ticking && !Multiplayer.ExecutingCmds) return false; - - return ignoreTraces == 0; - } - } - - [HarmonyPatch(typeof(UniqueIDsManager), nameof(UniqueIDsManager.GetNextID))] - public static class UniqueIdsPatch - { - static void Postfix() - { - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Thing), nameof(Thing.SpawnSetup))] - public static class ThingSpawnPatch - { - static void Postfix(Thing __instance) - { - if (__instance.def.HasThingIDNumber) - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] - public static class ThingDeSpawnPatch - { - static void Postfix(Thing __instance) - { - if (__instance.def.HasThingIDNumber) - DeferredStackTracing.Postfix(); - } - } - - [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] - public static class EndCurrentJobPatch - { - static void Prefix(Pawn_JobTracker __instance) - { - if (MpVersion.IsDebug && __instance.curJob != null && DeferredStackTracing.ShouldAddStackTraceForDesyncLog()) - Multiplayer.game.sync.TryAddInfoForDesyncLog($"EndCurrentJob for {__instance.pawn}: {__instance.curJob}", ""); - } - } - - [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] - static class WildAnimalSpawnerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(WildPlantSpawner), nameof(WildPlantSpawner.WildPlantSpawnerTick))] - static class WildPlantSpawnerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(SteadyEnvironmentEffects), nameof(SteadyEnvironmentEffects.SteadyEnvironmentEffectsTick))] - static class SteadyEnvironmentEffectsTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - [HarmonyPatch(typeof(TempTerrainManager), nameof(TempTerrainManager.Tick))] - static class TempTerrainManagerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } - - /*[HarmonyPatch(typeof(StoreUtility), nameof(StoreUtility.TryFindBestBetterStoreCellForWorker))] - static class FindBestStorageCellTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - }*/ - - [HarmonyPatch(typeof(IntermittentSteamSprayer), nameof(IntermittentSteamSprayer.SteamSprayerTick))] - static class SteamSprayerTickTraceIgnore - { - static void Prefix() => DeferredStackTracing.ignoreTraces++; - static void Finalizer() => DeferredStackTracing.ignoreTraces--; - } -} + var trace = logItem.raw; + int hash = 0; + // Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value. + int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash, skipFrames: 1); + + Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); + + acc++; + } + + public static bool ShouldAddStackTraceForDesyncLog() + { + if (!SupportsDeferredStackTracing) return false; + if (Multiplayer.Client == null) return false; + if (Multiplayer.settings.desyncTracingMode == DesyncTracingMode.None) return false; + if (Multiplayer.game == null) return false; + + if (!Multiplayer.game.gameComp.logDesyncTraces) return false; + + if (Rand.stateStack.Count > 1) return false; + if (Multiplayer.IsReplay) return false; + + if (!Multiplayer.Ticking && !Multiplayer.ExecutingCmds) return false; + + return ignoreTraces == 0; + } + } + + [HarmonyPatch(typeof(UniqueIDsManager), nameof(UniqueIDsManager.GetNextID))] + public static class UniqueIdsPatch + { + static void Postfix() + { + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.SpawnSetup))] + public static class ThingSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] + public static class ThingDeSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] + public static class EndCurrentJobPatch + { + static void Prefix(Pawn_JobTracker __instance) + { + if (MpVersion.IsDebug && __instance.curJob != null && DeferredStackTracing.ShouldAddStackTraceForDesyncLog()) + Multiplayer.game.sync.TryAddInfoForDesyncLog($"EndCurrentJob for {__instance.pawn}: {__instance.curJob}", ""); + } + } + + [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] + static class WildAnimalSpawnerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(WildPlantSpawner), nameof(WildPlantSpawner.WildPlantSpawnerTick))] + static class WildPlantSpawnerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(SteadyEnvironmentEffects), nameof(SteadyEnvironmentEffects.SteadyEnvironmentEffectsTick))] + static class SteadyEnvironmentEffectsTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + [HarmonyPatch(typeof(TempTerrainManager), nameof(TempTerrainManager.Tick))] + static class TempTerrainManagerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } + + /*[HarmonyPatch(typeof(StoreUtility), nameof(StoreUtility.TryFindBestBetterStoreCellForWorker))] + static class FindBestStorageCellTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + }*/ + + [HarmonyPatch(typeof(IntermittentSteamSprayer), nameof(IntermittentSteamSprayer.SteamSprayerTick))] + static class SteamSprayerTickTraceIgnore + { + static void Prefix() => DeferredStackTracing.ignoreTraces++; + static void Finalizer() => DeferredStackTracing.ignoreTraces--; + } +} diff --git a/Source/Client/Desyncs/JittedMethods.cs b/Source/Client/Desyncs/JittedMethods.cs index 7b966c1a4..17976caf7 100644 --- a/Source/Client/Desyncs/JittedMethods.cs +++ b/Source/Client/Desyncs/JittedMethods.cs @@ -1,116 +1,116 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using HarmonyLib; -using JetBrains.Annotations; -using Verse; - -namespace Multiplayer.Client.Desyncs; - -public class JittedMethod -{ - [CanBeNull] public MethodBase method; - [CanBeNull] public MethodBase from; - [CanBeNull] public int[] mapTicks; - public int worldTicks; - public int timer; - public bool inInterface; - - public string TimeString() - { - return $"m:{mapTicks?.Join(delimiter: ",") ?? "[]"} w:{worldTicks} t:{timer}"; - } - - public override string ToString() - { - return $"{method?.DeclaringType}.{method?.Name} from {from?.DeclaringType}.{from?.Name} {TimeString()} i:{inInterface}"; - } -} - -public static class JittedMethods -{ - private static Queue methodQueue = new(); - private static IntPtr profiler; - private static bool adding; - +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; +using JetBrains.Annotations; +using Verse; + +namespace Multiplayer.Client.Desyncs; + +public class JittedMethod +{ + [CanBeNull] public MethodBase method; + [CanBeNull] public MethodBase from; + [CanBeNull] public int[] mapTicks; + public int worldTicks; + public int timer; + public bool inInterface; + + public string TimeString() + { + return $"m:{mapTicks?.Join(delimiter: ",") ?? "[]"} w:{worldTicks} t:{timer}"; + } + + public override string ToString() + { + return $"{method?.DeclaringType}.{method?.Name} from {from?.DeclaringType}.{from?.Name} {TimeString()} i:{inInterface}"; + } +} + +public static class JittedMethods +{ + private static Queue methodQueue = new(); + private static IntPtr profiler; + private static bool adding; + public static void Init() { if (!Native.IsMonoRuntime) return; - profiler = Native.mono_profiler_create(IntPtr.Zero); - Native.mono_profiler_set_jit_done_callback(profiler, (_, method, _) => - { - if (adding) return; - - try - { - adding = true; - - if (UnityData.IsInMainThread && Multiplayer.settings != null && Native.mono_method_get_token(method) != 0) - { - var methodBase = Native.GetMethodBaseFromRuntimePointer(method); - methodQueue.Enqueue(new JittedMethod - { - method = methodBase, - from = new StackTrace().GetFrame(1)?.GetMethod(), - mapTicks = Multiplayer.game?.asyncTimeComps.Select(c => c.mapTicks).ToArray(), - worldTicks = Multiplayer.game?.asyncWorldTimeComp?.worldTicks ?? -1, - timer = TickPatch.Timer, - inInterface = Multiplayer.InInterface - }); - - if (methodQueue.Count > Multiplayer.settings.jittedMethodsInDesync) - methodQueue.Dequeue(); - } - } - finally - { - adding = false; - } - }); - } - + profiler = Native.mono_profiler_create(IntPtr.Zero); + Native.mono_profiler_set_jit_done_callback(profiler, (_, method, _) => + { + if (adding) return; + + try + { + adding = true; + + if (UnityData.IsInMainThread && Multiplayer.settings != null && Native.mono_method_get_token(method) != 0) + { + var methodBase = Native.GetMethodBaseFromRuntimePointer(method); + methodQueue.Enqueue(new JittedMethod + { + method = methodBase, + from = new StackTrace().GetFrame(1)?.GetMethod(), + mapTicks = Multiplayer.game?.asyncTimeComps.Select(c => c.mapTicks).ToArray(), + worldTicks = Multiplayer.game?.asyncWorldTimeComp?.worldTicks ?? -1, + timer = TickPatch.Timer, + inInterface = Multiplayer.InInterface + }); + + if (methodQueue.Count > Multiplayer.settings.jittedMethodsInDesync) + methodQueue.Dequeue(); + } + } + finally + { + adding = false; + } + }); + } + public static void OnApplicationQuit() { if (!Native.IsMonoRuntime || profiler == IntPtr.Zero) return; Native.mono_profiler_set_jit_done_callback(profiler, null); - } - + } + public static string GetJittedMethodsString() { if (!Native.IsMonoRuntime) return "Mono JIT history is unavailable on this runtime."; - // Two ToArrays to prevent a compilation in between from causing a "Collection was modified" exception - var jittedMethods = methodQueue.ToArray().Select((j, i) => (j, i)).ToArray(); - - var builder = new StringBuilder(); - builder.Append("In simulation:"); // This will get a newline from the != comparison below - var timeString = ""; - - foreach (var j in jittedMethods.Where(j => !j.j.inInterface)) - { - if (timeString != j.j.TimeString()) - builder.AppendLine(); - builder.AppendLine($"{j.i} {j.j}"); - timeString = j.j.TimeString(); - } - - builder.AppendLine(); - builder.AppendLine(); - - timeString = ""; - - builder.Append("In interface:"); - foreach (var j in jittedMethods.Where(j => j.j.inInterface)) - { - if (timeString != j.j.TimeString()) - builder.AppendLine(); - builder.AppendLine($"{j.i} {j.j}"); - timeString = j.j.TimeString(); - } - - return builder.ToString(); - } -} + // Two ToArrays to prevent a compilation in between from causing a "Collection was modified" exception + var jittedMethods = methodQueue.ToArray().Select((j, i) => (j, i)).ToArray(); + + var builder = new StringBuilder(); + builder.Append("In simulation:"); // This will get a newline from the != comparison below + var timeString = ""; + + foreach (var j in jittedMethods.Where(j => !j.j.inInterface)) + { + if (timeString != j.j.TimeString()) + builder.AppendLine(); + builder.AppendLine($"{j.i} {j.j}"); + timeString = j.j.TimeString(); + } + + builder.AppendLine(); + builder.AppendLine(); + + timeString = ""; + + builder.Append("In interface:"); + foreach (var j in jittedMethods.Where(j => j.j.inInterface)) + { + if (timeString != j.j.TimeString()) + builder.AppendLine(); + builder.AppendLine($"{j.i} {j.j}"); + timeString = j.j.TimeString(); + } + + return builder.ToString(); + } +} diff --git a/Source/Client/Desyncs/SaveableDesyncInfo.cs b/Source/Client/Desyncs/SaveableDesyncInfo.cs index 2bc80eba8..7379d9b65 100644 --- a/Source/Client/Desyncs/SaveableDesyncInfo.cs +++ b/Source/Client/Desyncs/SaveableDesyncInfo.cs @@ -1,201 +1,201 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using HarmonyLib; -using JetBrains.Annotations; -using Multiplayer.Common; -using Multiplayer.Common.Util; -using RimWorld; -using UnityEngine; -using Verse; -using CompressionLevel = System.IO.Compression.CompressionLevel; - -namespace Multiplayer.Client.Desyncs; - -public class SaveableDesyncInfo( - SyncCoordinator coordinator, - ClientSyncOpinion local, - ClientSyncOpinion remote, - int diffAt, - bool diffAtFound) -{ - public readonly ClientSyncOpinion local = local; - public readonly ClientSyncOpinion remote = remote; - public readonly int diffAt = diffAt; - private readonly Task metadata = Task.Run(MetadataGenerator.Generate); - private readonly Task replay = Task.Run(SaveReplayIfApplicable); - - public bool ReadyToSave => metadata.IsCompleted && replay.IsCompleted; - - public void Save([CanBeNull] HostInfo hostInfo) - { - var watch = Stopwatch.StartNew(); - - try - { - var desyncFilePath = FindFileNameForNextDesyncFile(); - using var zip = MpZipFile.Open(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip"), ZipArchiveMode.Create); - - zip.AddEntry("desync_info", GetDesyncDetails()); - - zip.AddEntry("local_traces.txt", GetLocalTraces()); - zip.AddEntry("host_traces.txt", hostInfo?.Traces ?? "No host traces"); - - zip.AddEntry("local_jitted_methods.txt", JittedMethods.GetJittedMethodsString()); - zip.AddEntry("host_jitted_methods.txt", hostInfo?.JittedMethods ?? "No host jitted methods"); - - var extraLogs = LogGenerator.PrepareLogData(); - if (extraLogs != null) zip.AddEntry("local_logs.txt", extraLogs); - - zip.AddEntry("local_metadata.txt", metadata.Result); - - try - { - replay.Wait(); - } - catch (AggregateException e) - { - if (e.InnerExceptions.SingleOrDefault(inner => inner is TaskCanceledException) == null) throw; - } - if (replay.IsCompletedSuccessfully) { - var replayFile = replay.Result; - zip.CreateEntryFromFile(replayFile.FullName, "replay.rwmts", CompressionLevel.NoCompression); - DeleteFileSilent(replayFile); - } - } - catch (Exception e) - { - Log.Error($"Exception writing desync info: {e}"); - } - - Log.Message($"Desync info writing took {watch.ElapsedMilliseconds}"); - } - - private string GetLocalTraces() - { - var traceMessage = ""; - var localCount = local.desyncStackTraceHashes.Count; - var remoteCount = remote.desyncStackTraceHashes.Count; - int count = Math.Min(localCount, remoteCount); - - if (diffAt == -1) - { - if (count == 0) - { - return $"No traces (remote: {remoteCount}, local: {localCount})"; - } - - traceMessage = "Note: trace hashes are equal between local and remote\n\n"; - } - else if (!diffAtFound) - { - traceMessage = "Note: traces differ in amount, but the existing ones are equal. This means that a tick" + - " has ended sooner on one of the connection sides\n\n"; - } - - traceMessage += local.GetFormattedStackTracesForRange(diffAt); - return traceMessage; - } - - private string GetDesyncDetails() - { - var desyncInfo = new StringBuilder(); - - desyncInfo - .AppendLine("###Tick Data###") - .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}") - .AppendLine($"Last Valid Tick - Local|||{coordinator.lastValidTick}") - .AppendLine($"Last Valid Tick - Arbiter|||{coordinator.arbiterWasPlayingOnLastValidTick}") - .AppendLine("\n###Version Data###") - .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") - .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") - .AppendLine("\n###Debug Options###") - .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") - .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") - .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") - .AppendLine("\n###Server Info###") - .AppendLine($"Player Count|||{Multiplayer.session.players.Count}") - .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") - .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") - .AppendLine($"Map Count|||{Find.Maps?.Count.ToStringSafe()}") - .AppendLine("\n###CPU Info###") - .AppendLine($"Processor Name|||{SystemInfo.processorType}") - .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}") - .AppendLine($"Thread Count|||{SystemInfo.processorCount}") - .AppendLine("\n###GPU Info###") +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using HarmonyLib; +using JetBrains.Annotations; +using Multiplayer.Common; +using Multiplayer.Common.Util; +using RimWorld; +using UnityEngine; +using Verse; +using CompressionLevel = System.IO.Compression.CompressionLevel; + +namespace Multiplayer.Client.Desyncs; + +public class SaveableDesyncInfo( + SyncCoordinator coordinator, + ClientSyncOpinion local, + ClientSyncOpinion remote, + int diffAt, + bool diffAtFound) +{ + public readonly ClientSyncOpinion local = local; + public readonly ClientSyncOpinion remote = remote; + public readonly int diffAt = diffAt; + private readonly Task metadata = Task.Run(MetadataGenerator.Generate); + private readonly Task replay = Task.Run(SaveReplayIfApplicable); + + public bool ReadyToSave => metadata.IsCompleted && replay.IsCompleted; + + public void Save([CanBeNull] HostInfo hostInfo) + { + var watch = Stopwatch.StartNew(); + + try + { + var desyncFilePath = FindFileNameForNextDesyncFile(); + using var zip = MpZipFile.Open(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip"), ZipArchiveMode.Create); + + zip.AddEntry("desync_info", GetDesyncDetails()); + + zip.AddEntry("local_traces.txt", GetLocalTraces()); + zip.AddEntry("host_traces.txt", hostInfo?.Traces ?? "No host traces"); + + zip.AddEntry("local_jitted_methods.txt", JittedMethods.GetJittedMethodsString()); + zip.AddEntry("host_jitted_methods.txt", hostInfo?.JittedMethods ?? "No host jitted methods"); + + var extraLogs = LogGenerator.PrepareLogData(); + if (extraLogs != null) zip.AddEntry("local_logs.txt", extraLogs); + + zip.AddEntry("local_metadata.txt", metadata.Result); + + try + { + replay.Wait(); + } + catch (AggregateException e) + { + if (e.InnerExceptions.SingleOrDefault(inner => inner is TaskCanceledException) == null) throw; + } + if (replay.IsCompletedSuccessfully) { + var replayFile = replay.Result; + zip.CreateEntryFromFile(replayFile.FullName, "replay.rwmts", CompressionLevel.NoCompression); + DeleteFileSilent(replayFile); + } + } + catch (Exception e) + { + Log.Error($"Exception writing desync info: {e}"); + } + + Log.Message($"Desync info writing took {watch.ElapsedMilliseconds}"); + } + + private string GetLocalTraces() + { + var traceMessage = ""; + var localCount = local.desyncStackTraceHashes.Count; + var remoteCount = remote.desyncStackTraceHashes.Count; + int count = Math.Min(localCount, remoteCount); + + if (diffAt == -1) + { + if (count == 0) + { + return $"No traces (remote: {remoteCount}, local: {localCount})"; + } + + traceMessage = "Note: trace hashes are equal between local and remote\n\n"; + } + else if (!diffAtFound) + { + traceMessage = "Note: traces differ in amount, but the existing ones are equal. This means that a tick" + + " has ended sooner on one of the connection sides\n\n"; + } + + traceMessage += local.GetFormattedStackTracesForRange(diffAt); + return traceMessage; + } + + private string GetDesyncDetails() + { + var desyncInfo = new StringBuilder(); + + desyncInfo + .AppendLine("###Tick Data###") + .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}") + .AppendLine($"Last Valid Tick - Local|||{coordinator.lastValidTick}") + .AppendLine($"Last Valid Tick - Arbiter|||{coordinator.arbiterWasPlayingOnLastValidTick}") + .AppendLine("\n###Version Data###") + .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") + .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") + .AppendLine("\n###Debug Options###") + .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") + .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") + .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") + .AppendLine("\n###Server Info###") + .AppendLine($"Player Count|||{Multiplayer.session.players.Count}") + .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") + .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") + .AppendLine($"Map Count|||{Find.Maps?.Count.ToStringSafe()}") + .AppendLine("\n###CPU Info###") + .AppendLine($"Processor Name|||{SystemInfo.processorType}") + .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}") + .AppendLine($"Thread Count|||{SystemInfo.processorCount}") + .AppendLine("\n###GPU Info###") #if !RIMWORLD_1_7 .AppendLine($"GPU Family|||{SystemInfo.graphicsDeviceVendor}") #endif - .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}") - .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}") - .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}") - .AppendLine("\n###RAM Info###") - .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}") - .AppendLine("\n###OS Info###") - .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") - .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}"); - - return desyncInfo.ToString(); - } - - private static string FindFileNameForNextDesyncFile() - { - const string FilePrefix = "Desync-"; - const string FileExtension = ".zip"; - - //Find all current existing desync zips - var files = new DirectoryInfo(Multiplayer.DesyncsDir).GetFiles($"{FilePrefix}*{FileExtension}"); - - const int MaxFiles = 10; - - //Delete any pushing us over the limit, and reserve room for one more - if (files.Length > MaxFiles - 1) - files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); - - //Find the current max desync number - int max = 0; - foreach (var f in files) - if (int.TryParse( - f.Name.Substring(FilePrefix.Length, f.Name.Length - FilePrefix.Length - FileExtension.Length), - out int result) && result > max) - max = result; - - return $"{FilePrefix}{max + 1:00}"; - } - - private static Task SaveReplayIfApplicable() - { - if (!Multiplayer.settings.includeReplayInDesync) return Task.FromCanceled(new CancellationToken(true)); - - try - { - var tmp = new FileInfo(Path.Combine(Multiplayer.DesyncsDir, "desync-replay.tmp.zip")); - if (tmp.Exists) tmp.Delete(); - Replay.ForSaving(tmp).WriteData(Multiplayer.session.dataSnapshot); - return Task.FromResult(tmp); - } - catch (Exception e) - { - Log.Error($"Failed to save replay for desync: {e}"); - return Task.FromException(e); - } - } - - private static void DeleteFileSilent(FileInfo file) - { - try - { - file.Delete(); - } - catch (IOException) - { - } - } - - public record HostInfo([CanBeNull] string Traces, [CanBeNull] string JittedMethods); -} + .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}") + .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}") + .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}") + .AppendLine("\n###RAM Info###") + .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}") + .AppendLine("\n###OS Info###") + .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") + .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}"); + + return desyncInfo.ToString(); + } + + private static string FindFileNameForNextDesyncFile() + { + const string FilePrefix = "Desync-"; + const string FileExtension = ".zip"; + + //Find all current existing desync zips + var files = new DirectoryInfo(Multiplayer.DesyncsDir).GetFiles($"{FilePrefix}*{FileExtension}"); + + const int MaxFiles = 10; + + //Delete any pushing us over the limit, and reserve room for one more + if (files.Length > MaxFiles - 1) + files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); + + //Find the current max desync number + int max = 0; + foreach (var f in files) + if (int.TryParse( + f.Name.Substring(FilePrefix.Length, f.Name.Length - FilePrefix.Length - FileExtension.Length), + out int result) && result > max) + max = result; + + return $"{FilePrefix}{max + 1:00}"; + } + + private static Task SaveReplayIfApplicable() + { + if (!Multiplayer.settings.includeReplayInDesync) return Task.FromCanceled(new CancellationToken(true)); + + try + { + var tmp = new FileInfo(Path.Combine(Multiplayer.DesyncsDir, "desync-replay.tmp.zip")); + if (tmp.Exists) tmp.Delete(); + Replay.ForSaving(tmp).WriteData(Multiplayer.session.dataSnapshot); + return Task.FromResult(tmp); + } + catch (Exception e) + { + Log.Error($"Failed to save replay for desync: {e}"); + return Task.FromException(e); + } + } + + private static void DeleteFileSilent(FileInfo file) + { + try + { + file.Delete(); + } + catch (IOException) + { + } + } + + public record HostInfo([CanBeNull] string Traces, [CanBeNull] string JittedMethods); +} diff --git a/Source/Client/Desyncs/StackTraceLogItem.cs b/Source/Client/Desyncs/StackTraceLogItem.cs index a57d6c69d..43d60ef0f 100644 --- a/Source/Client/Desyncs/StackTraceLogItem.cs +++ b/Source/Client/Desyncs/StackTraceLogItem.cs @@ -1,95 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Multiplayer.Client.Desyncs; -using Verse; - -namespace Multiplayer.Client -{ - public abstract class StackTraceLogItem : IDisposable - { - public int tick; - public int hash; - - public abstract string AdditionalInfo { get; } - - public abstract string StackTraceString { get; } - - public override string ToString() - { - return $"Tick:{tick} Hash:{hash} '{AdditionalInfo}'\n{StackTraceString}"; - } - - public virtual void Dispose() - { - } - } - - public class StackTraceLogItemObj : StackTraceLogItem - { - public string info1; - public string info2; - - public override string AdditionalInfo => $"{info1} {info2}"; - - public override string StackTraceString => ""; - } - - public class StackTraceLogItemRaw : StackTraceLogItem - { - public const int MaxDepth = 32; - public long[] raw = new long[MaxDepth]; +using System; +using System.Collections.Generic; +using System.Text; +using Multiplayer.Client.Desyncs; +using Verse; + +namespace Multiplayer.Client +{ + public abstract class StackTraceLogItem : IDisposable + { + public int tick; + public int hash; + + public abstract string AdditionalInfo { get; } + + public abstract string StackTraceString { get; } + + public override string ToString() + { + return $"Tick:{tick} Hash:{hash} '{AdditionalInfo}'\n{StackTraceString}"; + } + + public virtual void Dispose() + { + } + } + + public class StackTraceLogItemObj : StackTraceLogItem + { + public string info1; + public string info2; + + public override string AdditionalInfo => $"{info1} {info2}"; + + public override string StackTraceString => ""; + } + + public class StackTraceLogItemRaw : StackTraceLogItem + { + public const int MaxDepth = 32; + public long[] raw = new long[MaxDepth]; public int depth; public string managedTrace; - - public int ticksGame; - public ulong rngState; - public ThingDef thingDef; - public int thingId; - public string factionName; - public string moreInfo; - - public override string AdditionalInfo => $"{ticksGame} {thingDef}{thingId} {factionName} {depth} {rngState} {moreInfo}"; - - private static Dictionary methodNameCache = new(); - - public override string StackTraceString - { + + public int ticksGame; + public ulong rngState; + public ThingDef thingDef; + public int thingId; + public string factionName; + public string moreInfo; + + public override string AdditionalInfo => $"{ticksGame} {thingDef}{thingId} {factionName} {depth} {rngState} {moreInfo}"; + + private static Dictionary methodNameCache = new(); + + public override string StackTraceString + { get { if (managedTrace != null) return managedTrace; var builder = new StringBuilder(); - for (int i = 0; i < depth; i++) - { - var addr = raw[i]; - - if (!methodNameCache.TryGetValue(addr, out string method)) - methodNameCache[addr] = method = Native.MethodNameFromAddr(raw[i], false); - - builder.AppendLine(method != null ? SyncCoordinator.MethodNameWithIL(method) : "Null"); - } - - return builder.ToString(); - } - } - - public static StackTraceLogItemRaw GetFromPool() - { - return SimplePool.Get(); - } - + for (int i = 0; i < depth; i++) + { + var addr = raw[i]; + + if (!methodNameCache.TryGetValue(addr, out string method)) + methodNameCache[addr] = method = Native.MethodNameFromAddr(raw[i], false); + + builder.AppendLine(method != null ? SyncCoordinator.MethodNameWithIL(method) : "Null"); + } + + return builder.ToString(); + } + } + + public static StackTraceLogItemRaw GetFromPool() + { + return SimplePool.Get(); + } + public override void Dispose() { managedTrace = null; depth = 0; - ticksGame = 0; - rngState = 0; - thingId = 0; - thingDef = null; - factionName = null; - moreInfo = null; - - SimplePool.Return(this); - } - } -} + ticksGame = 0; + rngState = 0; + thingId = 0; + thingDef = null; + factionName = null; + moreInfo = null; + + SimplePool.Return(this); + } + } +} diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 51e3f143e..2d5ef474b 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,959 +1,959 @@ -using HarmonyLib; -using KTrie; -using Multiplayer.API; -using Multiplayer.Client.Factions; -using RimWorld; -using RimWorld.Planet; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; - -using UnityEngine; -using Verse; -using Verse.AI; -using Verse.Sound; -using static HarmonyLib.AccessTools; - -namespace Multiplayer.Client.Patches; - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRow))] -public static class MainTabWindow_QuestsDoRowPatch -{ - public static void Prefix(ref Rect rect, Quest quest) - { - if (Multiplayer.Client != null && !Multiplayer.settings.hideOtherPlayersQuests) - { - Faction playerFaction; - if (quest.TryGetPlayerFaction(out playerFaction)) - { - Rect iconRect = new Rect(rect.x + 2f, rect.y + 2f, 4f, rect.height - 4f); - Widgets.DrawBoxSolid(iconRect, playerFaction.Color); - rect.xMin += 8f; - TooltipHandler.TipRegion(rect, "MpQuestDesc".Translate(playerFaction.Name, playerFaction == Faction.OfPlayer ? ". (you)" : ".")); - } - else - { - TooltipHandler.TipRegion(rect, "MpPublicQuest".Translate()); - } - } - } -} - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.ShouldListNow))] -public static class MainTabWindow_QuestsShouldListNowPatch -{ - static bool Prefix(Quest quest, ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.settings.hideOtherPlayersQuests) - { - Faction playerFaction; - if (quest.TryGetPlayerFaction(out playerFaction) && playerFaction != Faction.OfPlayer) - { - __result = false; - return false; - } - } - return true; - } -} - -[HarmonyPatch(typeof(WorldObject), nameof(WorldObject.ShowRelatedQuests), MethodType.Getter)] -static class WorldObjectShowRelatedQuestsPatch -{ - static void Postfix(ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result = false; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.ShowRelatedQuests), MethodType.Getter)] -static class SettlementShowRelatedQuestsPatch -{ - static void Postfix(ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result = false; - } -} - -[HarmonyPatch(typeof(WorldInspectPane), nameof(WorldInspectPane.PaneTopY), MethodType.Getter)] -static class WorldInspectPanePaneTopYPatch -{ - static void Postfix(ref float __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - __result += 35f; - } -} - -[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRewardsPrefsButton))] -public static class MainTabWindow_QuestsDoRewardsPrefsButtonPatch -{ - public static bool Prefix(MainTabWindow_Quests __instance, Rect rect, out Rect __result) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) - { - __result = rect; - return true; - } - rect.yMin = rect.yMax - 40f; - - float buttonHeight = 40f; - - float rewardButtonWidth = rect.width * 0.5f - 5f; - float toggleButtonWidth = rect.width - rewardButtonWidth - 10f; - - Rect rewardRect = new Rect(rect.x, rect.yMax - buttonHeight, rewardButtonWidth, buttonHeight); - Text.Font = GameFont.Small; - if (Widgets.ButtonText(rewardRect, "ChooseRewards".Translate(), true, true, true)) - { - Find.WindowStack.Add(new Dialog_RewardPrefsConfig()); - } - - Rect toggleRect = new Rect(rewardRect.xMax + 10f, rewardRect.y, toggleButtonWidth, buttonHeight); - bool value = Multiplayer.settings.hideOtherPlayersQuests; - string label = value ? "MpHideQuestsOn".Translate() : "MpHideQuestsOff".Translate(); - if (Widgets.ButtonText(toggleRect, label, true, true)) - { - Multiplayer.settings.hideOtherPlayersQuests = !value; - SoundDefOf.Tick_High.PlayOneShotOnCamera(); - } - TooltipHandler.TipRegion(toggleRect, "MpHideQuestsDesc".Translate()); - - __result = rect; - return false; - } -} - -[HarmonyPatch(typeof(ColonistBar), nameof(ColonistBar.CheckRecacheEntries))] -public static class ColonistBarCheckRecacheEntriesPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var findMapsGetter = AccessTools.PropertyGetter(typeof(Find), nameof(Find.Maps)); - foreach (var inst in insts) - { - if (inst.Calls(findMapsGetter)) - { - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(ColonistBarCheckRecacheEntriesPatch), nameof(GetFilteredMaps))); - } - else - { - yield return inst; - } - } - } - - public static IEnumerable GetFilteredMaps() - { - if (Multiplayer.Client == null || !Multiplayer.settings.hideOtherPlayersInColonistBar) - return Find.Maps; - - return Find.Maps.Where(map => - map.mapPawns.FreeColonistsSpawned.Any() || - map.Parent?.Faction == Faction.OfPlayer); - } -} - -[HarmonyPatch(typeof(WorldRoutePlanner), nameof(WorldRoutePlanner.DoRoutePlannerButton))] -public static class WorldRoutePlannerPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || Multiplayer.RealPlayerFaction != Multiplayer.WorldComp.spectatorFaction; - } -} - -[HarmonyPatch(typeof(WorldGizmoUtility), nameof(WorldGizmoUtility.TryGetCaravanGizmo))] -public static class WorldGizmoUtilityTryGetCaravanGizmoPatch -{ - static bool Prefix(ref Gizmo gizmo, ref bool __result) - { - if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - { - gizmo = null; - __result = false; - return false; - } - return true; - } -} - -[HarmonyPatch(typeof(MainButtonsRoot), nameof(MainButtonsRoot.DoButtons))] -static class MainButtonsRootDoButtonsPatch -{ - private static bool IsSpectator => - Multiplayer.Client != null && - Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction; - - private static readonly string[] SpectatorButtons = { "Menu", "World" }; - - private static readonly FieldRef> AllButtonsRef = AccessTools.FieldRefAccess>("allButtonsInOrder"); - - private static readonly Dictionary SpectatorButtonOrder = SpectatorButtons.Select((name, idx) => new { name, idx }).ToDictionary(x => x.name, x => x.idx); - - [HarmonyPrefix] - private static bool ReplaceOriginalDrawing(MainButtonsRoot __instance) - { - if (!IsSpectator) - return true; - - try - { - var allButtons = AllButtonsRef(__instance); - if (allButtons == null) - return true; - - var toDraw = SpectatorButtons - .Select(name => allButtons.Find(b => b.defName == name)) - .Where(b => b?.Worker.Visible ?? false) - .ToList(); - - DrawCornerButtons(toDraw); - return false; - } - catch (Exception ex) - { - Log.Error($"[MainButtonsRootPatch] ReplaceOriginalDrawing failed: {ex}"); - return true; - } - } - - [HarmonyPostfix] - private static void FilterButtons(MainButtonsRoot __instance) - { - try - { - var allButtons = AllButtonsRef(__instance); - if (allButtons == null) - return; - - if (IsSpectator) - { - // Keep only SpectatorButtons, sorted by our map - var filtered = allButtons - .Where(b => SpectatorButtonOrder.ContainsKey(b.defName)) - .OrderBy(b => SpectatorButtonOrder[b.defName]) - .ToList(); - - AllButtonsRef(__instance) = filtered; - } - else - { - // Restore original full list in normal mode - var original = DefDatabase.AllDefsListForReading - .Where(b => b.order >= 0) - .OrderBy(b => b.order) - .ToList(); - - AllButtonsRef(__instance) = original; - } - } - catch (Exception ex) - { - Log.Error($"[MainButtonsRootPatch] FilterButtons failed: {ex}"); - } - } - - private static void DrawCornerButtons(List buttons) - { - const float buttonHeight = 35f; - const float defaultPadding = 20f; - const float spacing = 0f; - - float x = UI.screenWidth; - float y = UI.screenHeight - buttonHeight; - - foreach (var btn in buttons) - { - float padding = btn.defName == "World" ? 120f : defaultPadding; - float width = btn.ShortenedLabelCapWidth + padding; - - x -= width; - btn.Worker.DoButton(new Rect(x, y, width, buttonHeight)); - x -= spacing; - } - } -} - - -[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] -static class DisableDraftGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) - { - return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); - } -} - -[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] -static class PawnChangeRelationGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) - { - foreach (var gizmo in gizmos) - yield return gizmo; - - if (Multiplayer.Client == null || Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) - yield break; - - if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) - { - var otherFaction = __instance.Faction; - - yield return new Command_Action - { - defaultLabel = "Change faction relation", - icon = MultiplayerStatic.ChangeRelationIcon, - action = () => - { - List list = new List - { - new(FactionRelationKind.Hostile.ToString(), () => - { - SetRelation(otherFaction, FactionRelationKind.Hostile); - }), - new(FactionRelationKind.Neutral.ToString(), () => - { - SetRelation(otherFaction, FactionRelationKind.Neutral); - }) - }; - - Find.WindowStack.Add(new FloatMenu(list)); - } - }; - } - } - - [SyncMethod] - static void SetRelation(Faction other, FactionRelationKind kind) - { - Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); - - foreach (Map map in Find.Maps) - map.attackTargetsCache.Notify_FactionHostilityChanged(Faction.OfPlayer, other); - } -} - -[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] -static class CheckDefeatedPatch -{ - static bool Prefix(Settlement factionBase) - { - return factionBase.Faction is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] -static class CheckRemoveMapNowPatch -{ - static bool Prefix(MapParent __instance) - { - if (Multiplayer.Client == null) return true; - if (__instance is Camp) return true; - - bool parentBelongsToNonPlayerFaction = __instance.Faction is not { IsPlayer: true }; - - return parentBelongsToNonPlayerFaction; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] -static class SettlementAttackablePatch -{ - static bool Prefix(Settlement __instance) => __instance.Faction is not { IsPlayer: true }; -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] -static class SettlementNullFactionPatch1 -{ - static bool Prefix(Settlement __instance, ref Material __result) - { - if (__instance.factionInt == null) - { - __result = BaseContent.BadMat; - return false; - } - - return true; - } -} - -[HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] -static class SettlementNullFactionPatch2 -{ - static bool Prefix(Settlement __instance, ref Texture2D __result) - { - if (__instance.factionInt == null) - { - __result = BaseContent.BadTex; - return false; - } - - return true; - } -} - -[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetNaturalGoodwill))] -static class GetNaturalGoodwillPatch -{ - static bool Prefix(Faction other) - { - return other is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetMaxGoodwill))] -static class GetMaxGoodwillPatch -{ - static bool Prefix(Faction other) - { - return other is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(ScenPart_StartingAnimal), nameof(ScenPart_StartingAnimal.PetWeight))] -static class StartingAnimalPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var playerFactionField = AccessTools.Field(typeof(GameInitData), nameof(GameInitData.playerFaction)); - var factionOfPlayer = FactionOfPlayer; - - foreach (var inst in insts) - { - yield return inst; - - if (inst.operand as FieldInfo == playerFactionField) - yield return new CodeInstruction(OpCodes.Call, factionOfPlayer.Method); - } - } - - static Faction FactionOfPlayer(Faction f) - { - return Faction.OfPlayer; - } -} - -[HarmonyPatch(typeof(Pawn), nameof(Pawn.IsColonist), MethodType.Getter)] -static class PawnIsColonistPatch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isPlayerMethodGetter = AccessTools.PropertyGetter(typeof(Faction), nameof(Faction.IsPlayer)); - var factionIsPlayer = FactionIsPlayer; - - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isPlayerMethodGetter) - inst.operand = factionIsPlayer.Method; - - yield return inst; - } - } - - static bool FactionIsPlayer(Faction f) - { - return f == Find.FactionManager.OfPlayer; - } -} - -[HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] -static class RecacheColonistBelieverCountPatch -{ - private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive_FreeColonists_NoCryptosleep)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == allColonists) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(ColonistsAllPlayerFactions)); - yield return inst; - } - } - - private static List colonistsAllFactions = new(); - - private static List ColonistsAllPlayerFactions() - { - colonistsAllFactions.Clear(); - - foreach (var p in PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive) - { - if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) - colonistsAllFactions.Add(p); - } - - return colonistsAllFactions; - } - - // Note: IsColonist is patched above to compare to Faction.OfPlayer - public static bool IsColonistAnyFaction(Pawn p) - { - if (p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike) - return !p.IsSlave || p.guest.SlaveIsSecure; - return false; - } - - public static bool IsColonyMechAnyFaction(Pawn p) - { - if (ModsConfig.BiotechActive && p.RaceProps.IsMechanoid && p.Faction is { IsPlayer: true } && p.MentalStateDef == null) - return p.HostFaction == null || p.IsSlave; - return false; - } - - public static bool IsColonySubhumanAnyFaction(Pawn p) - { - return p.IsSubhuman && p.Faction is { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.AnyPawnBlockingMapRemoval), MethodType.Getter)] -static class AnyPawnBlockingMapRemovalPatch -{ - private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); - private static MethodInfo isColonyMech = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMech)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isColonyMech) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMechAnyFaction)); - - yield return inst; - } - } - - static void Postfix(MapPawns __instance, ref bool __result) - { - for (int i = 0; i < __instance.pawnsSpawned.Count; i++) - { - var p = __instance.pawnsSpawned[i]; - if (p.Faction is { IsPlayer: true } || p.HostFaction is { IsPlayer: true }) - { - Job curJob = p.CurJob; - if (curJob is { exitMapOnArrival: true }) - { - __result = true; - break; - } - - if (p.health.hediffSet.InLabor()) - { - __result = true; - break; - } - } - } - } -} - -[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.IsValidColonyPawn))] -static class IsValidColonyPawnPatch -{ - private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); - private static MethodInfo isColonySubhuman = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonySubhuman)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isColonist) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); - - if (inst.operand as MethodInfo == isColonySubhuman) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhumanAnyFaction)); - - yield return inst; - } - } -} - -[HarmonyPatch(typeof(Precept_Role), nameof(Precept_Role.ValidatePawn))] -static class ValidatePawnPatch -{ - private static MethodInfo isFreeNonSlaveColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeNonSlaveColonist)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == isFreeNonSlaveColonist) - inst.operand = AccessTools.Method(typeof(ValidatePawnPatch), nameof(IsFreeNonSlaveColonistAnyFaction)); - - yield return inst; - } - } - - public static bool IsFreeNonSlaveColonistAnyFaction(Pawn p) - { - return p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike && p.HostFaction == null && !p.IsSlave; - } -} - -[HarmonyPatch(typeof(Faction), nameof(Faction.HasGoodwill), MethodType.Getter)] -static class PlayerFactionsHaveGoodwill -{ - static void Postfix(Faction __instance, ref bool __result) - { - if (__instance.IsPlayer) - __result = true; - } -} - -[HarmonyPatch(typeof(GenHostility), nameof(GenHostility.IsActiveThreatToPlayer))] -static class IsActiveThreatToAnyPlayer -{ - static void Postfix(IAttackTarget target, ref bool __result) - { - foreach (var f in Find.FactionManager.AllFactions) - if (f.IsPlayer) - __result |= GenHostility.IsActiveThreatTo(target, f); - } -} - -[HarmonyPatch] -static class TakeToBedGuestFactionPatch -{ - static IEnumerable TargetMethods() - { - yield return AccessTools.DeclaredMethod(typeof(JobDriver_TakeToBed), nameof(JobDriver_TakeToBed.CheckMakeTakeeGuest)); - yield return AccessTools.DeclaredMethod(typeof(JobDriver_CarryDownedPawn), nameof(JobDriver_CarryDownedPawn.CheckMakeTakeeGuest)); - } - static bool Prefix(JobDriver __instance) - { - var takee = __instance.job.GetTarget(TargetIndex.A).Pawn; - return takee?.Faction?.IsPlayer != true && takee?.HostFaction?.IsPlayer != true; - } -} - - -[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] -static class LetterStackReceiveOnlyMyFaction -{ - // todo the letter might get culled from the archive if it isn't in the stack and Sync depends on the archive - static void Postfix(LetterStack __instance, Letter let) - { - if (Multiplayer.RealPlayerFaction != Faction.OfPlayer) - __instance.letters.Remove(let); - } -} - -[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] -static class LetterStackReceiveSoundOnlyMyFaction -{ - private static MethodInfo PlayOneShotOnCamera = - typeof(SoundStarter).GetMethod(nameof(SoundStarter.PlayOneShotOnCamera)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - if (inst.operand as MethodInfo == PlayOneShotOnCamera) - yield return new CodeInstruction( - OpCodes.Call, - SymbolExtensions.GetMethodInfo((SoundDef s, Map m) => PlaySoundReplacement(s, m))); - else - yield return inst; - } - } - - static void PlaySoundReplacement(SoundDef sound, Map map) - { - if (Multiplayer.RealPlayerFaction == Faction.OfPlayer) - sound.PlayOneShotOnCamera(map); - } -} - -[HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] -static class ApparelWornGraphicPathGetterPatch -{ - private static FieldInfo thingIDNumberField = AccessTools.Field(typeof(Thing), nameof(Thing.thingIDNumber)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - yield return inst; - - // This instruction is part of wornGraphicPaths[thingIDNumber % wornGraphicPaths.Count] - // The function makes sure the id is positive - if (inst.operand as FieldInfo == thingIDNumberField) - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(ApparelWornGraphicPathGetterPatch), nameof(MakeIdPositive))); - } - } - - private static int MakeIdPositive(int id) - { - return id < 0 ? -id : id; - } -} - -#region Map generation - -[HarmonyPatch(typeof(GenStep_Monolith), nameof(GenStep_Monolith.GenerateMonolith))] -public static class GenStepMonolithPatch -{ - static bool Prefix() - { - if (Multiplayer.Client == null) - return true; - - var anomalyComp = Current.Game.components.OfType().FirstOrDefault(); - - return anomalyComp?.monolith == null; - } -} - -[HarmonyPatch(typeof(ScenPart_ScatterThings), nameof(ScenPart_ScatterThings.GenerateIntoMap))] -static class ScenPartScatterThingsPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || FactionCreator.generatingMap; - } -} - -[HarmonyPatch(typeof(ScenPart_PlayerPawnsArriveMethod), nameof(ScenPart_PlayerPawnsArriveMethod.GenerateIntoMap))] -static class ScenPartPlayerPawnsArriveMethodPatch -{ - static bool Prefix() - { - return Multiplayer.Client == null || FactionCreator.generatingMap; - } -} - -#endregion - -[HarmonyPatch(typeof(CharacterCardUtility), nameof(CharacterCardUtility.DoTopStack))] -static class CharacterCardUtilityDontDrawIdeoPlate -{ - private static FieldInfo classicModeField = AccessTools.Field(typeof(IdeoManager), nameof(IdeoManager.classicMode)); - - static IEnumerable Transpiler(IEnumerable insts) - { - foreach (var inst in insts) - { - yield return inst; - - // Don't draw the ideo plate while choosing starting pawns in multifaction - if (inst.operand as FieldInfo == classicModeField) - { - yield return new CodeInstruction(OpCodes.Ldarg_2); - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(CharacterCardUtilityDontDrawIdeoPlate), nameof(DontDrawIdeoPlate))); - yield return new CodeInstruction(OpCodes.Or); - } - } - } - - private static bool DontDrawIdeoPlate(bool generating) - { - return Multiplayer.Client != null && generating; - } -} - -[HarmonyPatch(typeof(CompShuttle), nameof(CompShuttle.ContainedColonistCount), MethodType.Getter)] -static class CompShuttle_ContainedColonistCount_Patch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); - var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), - nameof(IsFreeColonistAnyPlayerFaction)); - - foreach (var ci in insts) - { - if (ci.Calls(isFreeColonist)) - { - ci.opcode = OpCodes.Call; - ci.operand = replacement; - } - yield return ci; - } - } - public static bool IsFreeColonistAnyPlayerFaction(Pawn pawn) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) - return pawn.IsFreeColonist; - - return pawn.Faction != null && - pawn.Faction.IsPlayer && - pawn.RaceProps.Humanlike && - (!pawn.IsSlave || pawn.guest.SlaveIsSecure) && - !pawn.IsSubhuman && - pawn.HostFaction == null; - } - -} - -[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.RemoveGravshipFromMap))] -static class RemoveGravshipFromMap_Patch -{ - static void Prefix(Building_GravEngine engine, out bool __state) - { - __state = false; - if (Multiplayer.Client == null) return; - if (!Multiplayer.GameComp.multifaction) return; - if (engine == null) return; - - engine.Map.PushFaction(engine.Faction); - - __state = true; - } - - static void Finalizer(Building_GravEngine engine, bool __state) - { - if (!__state) return; - engine.Map.PopFaction(); - } -} - -[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] -static class PlaceGravship_FactionContext_Patch -{ - static void Prefix(Gravship gravship, Map map, out bool __state) - { - __state = false; - if (Multiplayer.Client == null) return; - if (!Multiplayer.GameComp.multifaction) return; - - map.PushFaction(gravship.Faction); - - __state = true; - } - - static void Finalizer(Gravship gravship, Map map, bool __state) - { - if (!__state) return; - map.PopFaction(); - } -} - -[HarmonyPatch(typeof(QuestPart_LendColonistsToFaction), nameof(QuestPart_LendColonistsToFaction.Enable))] -static class QuestPart_LendColonistsToFaction_Enable_Patch -{ - static IEnumerable Transpiler(IEnumerable insts) - { - var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); - var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), - nameof(CompShuttle_ContainedColonistCount_Patch.IsFreeColonistAnyPlayerFaction)); - foreach (var ci in insts) - { - if (ci.Calls(isFreeColonist)) - { - ci.opcode = OpCodes.Call; - ci.operand = replacement; - } - yield return ci; - } - } -} - -[HarmonyPatch(typeof(Map), nameof(Map.IsPlayerHome), MethodType.Getter)] -static class Map_IsPlayerHome_Spectator_Patch -{ - static bool Prefix(Map __instance, ref bool __result) - { - if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction || - Faction.OfPlayer != Multiplayer.WorldComp.spectatorFaction) - return true; - - { - // Is repeat through all player faction a better idea? - if (!__instance.wasSpawnedViaGravShipLanding) - { - MapInfo mapInfo = __instance.info; - if (((mapInfo != null) ? mapInfo.parent : null) == null || __instance.info.parent.Faction?.IsPlayer != true || !__instance.info.parent.def.canBePlayerHome) - { - __result = GravshipUtility.PlayerHasGravEngine(__instance); - return false; - } - } - __result = true; - return false; - } - } -} - -[HarmonyPatch] -static class Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch -{ - static MethodInfo TargetMethod() - { - return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(Pawn_TraderTracker), nameof(Pawn_TraderTracker.ColonyThingsWillingToBuy))); - } - public static List AllBuildingsColonistOfDefOfPlayer(this ListerBuildings lister, ThingDef def) - { - if (def.CanHaveFaction) - return lister.AllBuildingsColonistOfDef(def).FindAll(building => building.Faction == Faction.OfPlayer); - else - return lister.AllBuildingsColonistOfDef(def); - } - public static IEnumerable AllColonistBuildingsOfTypeOfPlayer(this ListerBuildings lister) - { - return lister.AllColonistBuildingsOfType().Where(t => - { - if (t is Building building && (!building.def.CanHaveFaction || building.Faction == Faction.OfPlayer)) - return true; - return false; - }); - } - static IEnumerable Transpiler(IEnumerable insts) - { - var allBuildingsColonistOfDef = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef)); - var allBuildingsColonistOfDefOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllBuildingsColonistOfDefOfPlayer)); - var allColonistBuildingsOfType = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllColonistBuildingsOfType)).MakeGenericMethod([typeof(IHaulSource)]); - var allColonistBuildingsOfTypeOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllColonistBuildingsOfTypeOfPlayer)).MakeGenericMethod([typeof(IHaulSource)]); - foreach (var ci in insts) - { - if (ci.Calls(allBuildingsColonistOfDef)) - { - ci.opcode = OpCodes.Call; - ci.operand = allBuildingsColonistOfDefOfPlayer; - } - else if (ci.Calls(allColonistBuildingsOfType)) - { - ci.opcode = OpCodes.Call; - ci.operand = allColonistBuildingsOfTypeOfPlayer; - } - yield return ci; - } - } -} - -[HarmonyPatch] -static class TradeUtility_AllLaunchableThingsForTrade_Patch -{ - static MethodInfo TargetMethod() - { - return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(TradeUtility), nameof(TradeUtility.AllLaunchableThingsForTrade))); - } - - public static IEnumerable AllPoweredOfPlayer(Map map) - { - return Building_OrbitalTradeBeacon.AllPowered(map).Where(beacon => beacon.Faction == Faction.OfPlayer); - } - static IEnumerable Transpiler(IEnumerable insts) - { - var allPowered = AccessTools.Method(typeof(Building_OrbitalTradeBeacon), nameof(Building_OrbitalTradeBeacon.AllPowered)); - var allPoweredOfPlayer = AccessTools.Method(typeof(TradeUtility_AllLaunchableThingsForTrade_Patch), nameof(TradeUtility_AllLaunchableThingsForTrade_Patch.AllPoweredOfPlayer)); - foreach (var ci in insts) - { - if (ci.Calls(allPowered)) - { - ci.opcode = OpCodes.Call; - ci.operand = allPoweredOfPlayer; - } - - yield return ci; - } - } -} +using HarmonyLib; +using KTrie; +using Multiplayer.API; +using Multiplayer.Client.Factions; +using RimWorld; +using RimWorld.Planet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +using UnityEngine; +using Verse; +using Verse.AI; +using Verse.Sound; +using static HarmonyLib.AccessTools; + +namespace Multiplayer.Client.Patches; + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRow))] +public static class MainTabWindow_QuestsDoRowPatch +{ + public static void Prefix(ref Rect rect, Quest quest) + { + if (Multiplayer.Client != null && !Multiplayer.settings.hideOtherPlayersQuests) + { + Faction playerFaction; + if (quest.TryGetPlayerFaction(out playerFaction)) + { + Rect iconRect = new Rect(rect.x + 2f, rect.y + 2f, 4f, rect.height - 4f); + Widgets.DrawBoxSolid(iconRect, playerFaction.Color); + rect.xMin += 8f; + TooltipHandler.TipRegion(rect, "MpQuestDesc".Translate(playerFaction.Name, playerFaction == Faction.OfPlayer ? ". (you)" : ".")); + } + else + { + TooltipHandler.TipRegion(rect, "MpPublicQuest".Translate()); + } + } + } +} + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.ShouldListNow))] +public static class MainTabWindow_QuestsShouldListNowPatch +{ + static bool Prefix(Quest quest, ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.settings.hideOtherPlayersQuests) + { + Faction playerFaction; + if (quest.TryGetPlayerFaction(out playerFaction) && playerFaction != Faction.OfPlayer) + { + __result = false; + return false; + } + } + return true; + } +} + +[HarmonyPatch(typeof(WorldObject), nameof(WorldObject.ShowRelatedQuests), MethodType.Getter)] +static class WorldObjectShowRelatedQuestsPatch +{ + static void Postfix(ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result = false; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.ShowRelatedQuests), MethodType.Getter)] +static class SettlementShowRelatedQuestsPatch +{ + static void Postfix(ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result = false; + } +} + +[HarmonyPatch(typeof(WorldInspectPane), nameof(WorldInspectPane.PaneTopY), MethodType.Getter)] +static class WorldInspectPanePaneTopYPatch +{ + static void Postfix(ref float __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + __result += 35f; + } +} + +[HarmonyPatch(typeof(MainTabWindow_Quests), nameof(MainTabWindow_Quests.DoRewardsPrefsButton))] +public static class MainTabWindow_QuestsDoRewardsPrefsButtonPatch +{ + public static bool Prefix(MainTabWindow_Quests __instance, Rect rect, out Rect __result) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) + { + __result = rect; + return true; + } + rect.yMin = rect.yMax - 40f; + + float buttonHeight = 40f; + + float rewardButtonWidth = rect.width * 0.5f - 5f; + float toggleButtonWidth = rect.width - rewardButtonWidth - 10f; + + Rect rewardRect = new Rect(rect.x, rect.yMax - buttonHeight, rewardButtonWidth, buttonHeight); + Text.Font = GameFont.Small; + if (Widgets.ButtonText(rewardRect, "ChooseRewards".Translate(), true, true, true)) + { + Find.WindowStack.Add(new Dialog_RewardPrefsConfig()); + } + + Rect toggleRect = new Rect(rewardRect.xMax + 10f, rewardRect.y, toggleButtonWidth, buttonHeight); + bool value = Multiplayer.settings.hideOtherPlayersQuests; + string label = value ? "MpHideQuestsOn".Translate() : "MpHideQuestsOff".Translate(); + if (Widgets.ButtonText(toggleRect, label, true, true)) + { + Multiplayer.settings.hideOtherPlayersQuests = !value; + SoundDefOf.Tick_High.PlayOneShotOnCamera(); + } + TooltipHandler.TipRegion(toggleRect, "MpHideQuestsDesc".Translate()); + + __result = rect; + return false; + } +} + +[HarmonyPatch(typeof(ColonistBar), nameof(ColonistBar.CheckRecacheEntries))] +public static class ColonistBarCheckRecacheEntriesPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var findMapsGetter = AccessTools.PropertyGetter(typeof(Find), nameof(Find.Maps)); + foreach (var inst in insts) + { + if (inst.Calls(findMapsGetter)) + { + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(ColonistBarCheckRecacheEntriesPatch), nameof(GetFilteredMaps))); + } + else + { + yield return inst; + } + } + } + + public static IEnumerable GetFilteredMaps() + { + if (Multiplayer.Client == null || !Multiplayer.settings.hideOtherPlayersInColonistBar) + return Find.Maps; + + return Find.Maps.Where(map => + map.mapPawns.FreeColonistsSpawned.Any() || + map.Parent?.Faction == Faction.OfPlayer); + } +} + +[HarmonyPatch(typeof(WorldRoutePlanner), nameof(WorldRoutePlanner.DoRoutePlannerButton))] +public static class WorldRoutePlannerPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || Multiplayer.RealPlayerFaction != Multiplayer.WorldComp.spectatorFaction; + } +} + +[HarmonyPatch(typeof(WorldGizmoUtility), nameof(WorldGizmoUtility.TryGetCaravanGizmo))] +public static class WorldGizmoUtilityTryGetCaravanGizmoPatch +{ + static bool Prefix(ref Gizmo gizmo, ref bool __result) + { + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + { + gizmo = null; + __result = false; + return false; + } + return true; + } +} + +[HarmonyPatch(typeof(MainButtonsRoot), nameof(MainButtonsRoot.DoButtons))] +static class MainButtonsRootDoButtonsPatch +{ + private static bool IsSpectator => + Multiplayer.Client != null && + Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction; + + private static readonly string[] SpectatorButtons = { "Menu", "World" }; + + private static readonly FieldRef> AllButtonsRef = AccessTools.FieldRefAccess>("allButtonsInOrder"); + + private static readonly Dictionary SpectatorButtonOrder = SpectatorButtons.Select((name, idx) => new { name, idx }).ToDictionary(x => x.name, x => x.idx); + + [HarmonyPrefix] + private static bool ReplaceOriginalDrawing(MainButtonsRoot __instance) + { + if (!IsSpectator) + return true; + + try + { + var allButtons = AllButtonsRef(__instance); + if (allButtons == null) + return true; + + var toDraw = SpectatorButtons + .Select(name => allButtons.Find(b => b.defName == name)) + .Where(b => b?.Worker.Visible ?? false) + .ToList(); + + DrawCornerButtons(toDraw); + return false; + } + catch (Exception ex) + { + Log.Error($"[MainButtonsRootPatch] ReplaceOriginalDrawing failed: {ex}"); + return true; + } + } + + [HarmonyPostfix] + private static void FilterButtons(MainButtonsRoot __instance) + { + try + { + var allButtons = AllButtonsRef(__instance); + if (allButtons == null) + return; + + if (IsSpectator) + { + // Keep only SpectatorButtons, sorted by our map + var filtered = allButtons + .Where(b => SpectatorButtonOrder.ContainsKey(b.defName)) + .OrderBy(b => SpectatorButtonOrder[b.defName]) + .ToList(); + + AllButtonsRef(__instance) = filtered; + } + else + { + // Restore original full list in normal mode + var original = DefDatabase.AllDefsListForReading + .Where(b => b.order >= 0) + .OrderBy(b => b.order) + .ToList(); + + AllButtonsRef(__instance) = original; + } + } + catch (Exception ex) + { + Log.Error($"[MainButtonsRootPatch] FilterButtons failed: {ex}"); + } + } + + private static void DrawCornerButtons(List buttons) + { + const float buttonHeight = 35f; + const float defaultPadding = 20f; + const float spacing = 0f; + + float x = UI.screenWidth; + float y = UI.screenHeight - buttonHeight; + + foreach (var btn in buttons) + { + float padding = btn.defName == "World" ? 120f : defaultPadding; + float width = btn.ShortenedLabelCapWidth + padding; + + x -= width; + btn.Worker.DoButton(new Rect(x, y, width, buttonHeight)); + x -= spacing; + } + } +} + + +[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] +static class DisableDraftGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) + { + return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] +static class PawnChangeRelationGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) + { + foreach (var gizmo in gizmos) + yield return gizmo; + + if (Multiplayer.Client == null || Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + yield break; + + if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) + { + var otherFaction = __instance.Faction; + + yield return new Command_Action + { + defaultLabel = "Change faction relation", + icon = MultiplayerStatic.ChangeRelationIcon, + action = () => + { + List list = new List + { + new(FactionRelationKind.Hostile.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Hostile); + }), + new(FactionRelationKind.Neutral.ToString(), () => + { + SetRelation(otherFaction, FactionRelationKind.Neutral); + }) + }; + + Find.WindowStack.Add(new FloatMenu(list)); + } + }; + } + } + + [SyncMethod] + static void SetRelation(Faction other, FactionRelationKind kind) + { + Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); + + foreach (Map map in Find.Maps) + map.attackTargetsCache.Notify_FactionHostilityChanged(Faction.OfPlayer, other); + } +} + +[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] +static class CheckDefeatedPatch +{ + static bool Prefix(Settlement factionBase) + { + return factionBase.Faction is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] +static class CheckRemoveMapNowPatch +{ + static bool Prefix(MapParent __instance) + { + if (Multiplayer.Client == null) return true; + if (__instance is Camp) return true; + + bool parentBelongsToNonPlayerFaction = __instance.Faction is not { IsPlayer: true }; + + return parentBelongsToNonPlayerFaction; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] +static class SettlementAttackablePatch +{ + static bool Prefix(Settlement __instance) => __instance.Faction is not { IsPlayer: true }; +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] +static class SettlementNullFactionPatch1 +{ + static bool Prefix(Settlement __instance, ref Material __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadMat; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] +static class SettlementNullFactionPatch2 +{ + static bool Prefix(Settlement __instance, ref Texture2D __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadTex; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetNaturalGoodwill))] +static class GetNaturalGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetMaxGoodwill))] +static class GetMaxGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(ScenPart_StartingAnimal), nameof(ScenPart_StartingAnimal.PetWeight))] +static class StartingAnimalPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var playerFactionField = AccessTools.Field(typeof(GameInitData), nameof(GameInitData.playerFaction)); + var factionOfPlayer = FactionOfPlayer; + + foreach (var inst in insts) + { + yield return inst; + + if (inst.operand as FieldInfo == playerFactionField) + yield return new CodeInstruction(OpCodes.Call, factionOfPlayer.Method); + } + } + + static Faction FactionOfPlayer(Faction f) + { + return Faction.OfPlayer; + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.IsColonist), MethodType.Getter)] +static class PawnIsColonistPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isPlayerMethodGetter = AccessTools.PropertyGetter(typeof(Faction), nameof(Faction.IsPlayer)); + var factionIsPlayer = FactionIsPlayer; + + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isPlayerMethodGetter) + inst.operand = factionIsPlayer.Method; + + yield return inst; + } + } + + static bool FactionIsPlayer(Faction f) + { + return f == Find.FactionManager.OfPlayer; + } +} + +[HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] +static class RecacheColonistBelieverCountPatch +{ + private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive_FreeColonists_NoCryptosleep)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == allColonists) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(ColonistsAllPlayerFactions)); + yield return inst; + } + } + + private static List colonistsAllFactions = new(); + + private static List ColonistsAllPlayerFactions() + { + colonistsAllFactions.Clear(); + + foreach (var p in PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive) + { + if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) + colonistsAllFactions.Add(p); + } + + return colonistsAllFactions; + } + + // Note: IsColonist is patched above to compare to Faction.OfPlayer + public static bool IsColonistAnyFaction(Pawn p) + { + if (p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike) + return !p.IsSlave || p.guest.SlaveIsSecure; + return false; + } + + public static bool IsColonyMechAnyFaction(Pawn p) + { + if (ModsConfig.BiotechActive && p.RaceProps.IsMechanoid && p.Faction is { IsPlayer: true } && p.MentalStateDef == null) + return p.HostFaction == null || p.IsSlave; + return false; + } + + public static bool IsColonySubhumanAnyFaction(Pawn p) + { + return p.IsSubhuman && p.Faction is { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.AnyPawnBlockingMapRemoval), MethodType.Getter)] +static class AnyPawnBlockingMapRemovalPatch +{ + private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); + private static MethodInfo isColonyMech = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMech)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isColonyMech) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMechAnyFaction)); + + yield return inst; + } + } + + static void Postfix(MapPawns __instance, ref bool __result) + { + for (int i = 0; i < __instance.pawnsSpawned.Count; i++) + { + var p = __instance.pawnsSpawned[i]; + if (p.Faction is { IsPlayer: true } || p.HostFaction is { IsPlayer: true }) + { + Job curJob = p.CurJob; + if (curJob is { exitMapOnArrival: true }) + { + __result = true; + break; + } + + if (p.health.hediffSet.InLabor()) + { + __result = true; + break; + } + } + } + } +} + +[HarmonyPatch(typeof(MapPawns), nameof(MapPawns.IsValidColonyPawn))] +static class IsValidColonyPawnPatch +{ + private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); + private static MethodInfo isColonySubhuman = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonySubhuman)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isColonist) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); + + if (inst.operand as MethodInfo == isColonySubhuman) + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhumanAnyFaction)); + + yield return inst; + } + } +} + +[HarmonyPatch(typeof(Precept_Role), nameof(Precept_Role.ValidatePawn))] +static class ValidatePawnPatch +{ + private static MethodInfo isFreeNonSlaveColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeNonSlaveColonist)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == isFreeNonSlaveColonist) + inst.operand = AccessTools.Method(typeof(ValidatePawnPatch), nameof(IsFreeNonSlaveColonistAnyFaction)); + + yield return inst; + } + } + + public static bool IsFreeNonSlaveColonistAnyFaction(Pawn p) + { + return p.Faction is { IsPlayer: true } && p.RaceProps.Humanlike && p.HostFaction == null && !p.IsSlave; + } +} + +[HarmonyPatch(typeof(Faction), nameof(Faction.HasGoodwill), MethodType.Getter)] +static class PlayerFactionsHaveGoodwill +{ + static void Postfix(Faction __instance, ref bool __result) + { + if (__instance.IsPlayer) + __result = true; + } +} + +[HarmonyPatch(typeof(GenHostility), nameof(GenHostility.IsActiveThreatToPlayer))] +static class IsActiveThreatToAnyPlayer +{ + static void Postfix(IAttackTarget target, ref bool __result) + { + foreach (var f in Find.FactionManager.AllFactions) + if (f.IsPlayer) + __result |= GenHostility.IsActiveThreatTo(target, f); + } +} + +[HarmonyPatch] +static class TakeToBedGuestFactionPatch +{ + static IEnumerable TargetMethods() + { + yield return AccessTools.DeclaredMethod(typeof(JobDriver_TakeToBed), nameof(JobDriver_TakeToBed.CheckMakeTakeeGuest)); + yield return AccessTools.DeclaredMethod(typeof(JobDriver_CarryDownedPawn), nameof(JobDriver_CarryDownedPawn.CheckMakeTakeeGuest)); + } + static bool Prefix(JobDriver __instance) + { + var takee = __instance.job.GetTarget(TargetIndex.A).Pawn; + return takee?.Faction?.IsPlayer != true && takee?.HostFaction?.IsPlayer != true; + } +} + + +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] +static class LetterStackReceiveOnlyMyFaction +{ + // todo the letter might get culled from the archive if it isn't in the stack and Sync depends on the archive + static void Postfix(LetterStack __instance, Letter let) + { + if (Multiplayer.RealPlayerFaction != Faction.OfPlayer) + __instance.letters.Remove(let); + } +} + +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string), typeof(int), typeof(bool))] +static class LetterStackReceiveSoundOnlyMyFaction +{ + private static MethodInfo PlayOneShotOnCamera = + typeof(SoundStarter).GetMethod(nameof(SoundStarter.PlayOneShotOnCamera)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand as MethodInfo == PlayOneShotOnCamera) + yield return new CodeInstruction( + OpCodes.Call, + SymbolExtensions.GetMethodInfo((SoundDef s, Map m) => PlaySoundReplacement(s, m))); + else + yield return inst; + } + } + + static void PlaySoundReplacement(SoundDef sound, Map map) + { + if (Multiplayer.RealPlayerFaction == Faction.OfPlayer) + sound.PlayOneShotOnCamera(map); + } +} + +[HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] +static class ApparelWornGraphicPathGetterPatch +{ + private static FieldInfo thingIDNumberField = AccessTools.Field(typeof(Thing), nameof(Thing.thingIDNumber)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + yield return inst; + + // This instruction is part of wornGraphicPaths[thingIDNumber % wornGraphicPaths.Count] + // The function makes sure the id is positive + if (inst.operand as FieldInfo == thingIDNumberField) + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(ApparelWornGraphicPathGetterPatch), nameof(MakeIdPositive))); + } + } + + private static int MakeIdPositive(int id) + { + return id < 0 ? -id : id; + } +} + +#region Map generation + +[HarmonyPatch(typeof(GenStep_Monolith), nameof(GenStep_Monolith.GenerateMonolith))] +public static class GenStepMonolithPatch +{ + static bool Prefix() + { + if (Multiplayer.Client == null) + return true; + + var anomalyComp = Current.Game.components.OfType().FirstOrDefault(); + + return anomalyComp?.monolith == null; + } +} + +[HarmonyPatch(typeof(ScenPart_ScatterThings), nameof(ScenPart_ScatterThings.GenerateIntoMap))] +static class ScenPartScatterThingsPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || FactionCreator.generatingMap; + } +} + +[HarmonyPatch(typeof(ScenPart_PlayerPawnsArriveMethod), nameof(ScenPart_PlayerPawnsArriveMethod.GenerateIntoMap))] +static class ScenPartPlayerPawnsArriveMethodPatch +{ + static bool Prefix() + { + return Multiplayer.Client == null || FactionCreator.generatingMap; + } +} + +#endregion + +[HarmonyPatch(typeof(CharacterCardUtility), nameof(CharacterCardUtility.DoTopStack))] +static class CharacterCardUtilityDontDrawIdeoPlate +{ + private static FieldInfo classicModeField = AccessTools.Field(typeof(IdeoManager), nameof(IdeoManager.classicMode)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + yield return inst; + + // Don't draw the ideo plate while choosing starting pawns in multifaction + if (inst.operand as FieldInfo == classicModeField) + { + yield return new CodeInstruction(OpCodes.Ldarg_2); + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(CharacterCardUtilityDontDrawIdeoPlate), nameof(DontDrawIdeoPlate))); + yield return new CodeInstruction(OpCodes.Or); + } + } + } + + private static bool DontDrawIdeoPlate(bool generating) + { + return Multiplayer.Client != null && generating; + } +} + +[HarmonyPatch(typeof(CompShuttle), nameof(CompShuttle.ContainedColonistCount), MethodType.Getter)] +static class CompShuttle_ContainedColonistCount_Patch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); + var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), + nameof(IsFreeColonistAnyPlayerFaction)); + + foreach (var ci in insts) + { + if (ci.Calls(isFreeColonist)) + { + ci.opcode = OpCodes.Call; + ci.operand = replacement; + } + yield return ci; + } + } + public static bool IsFreeColonistAnyPlayerFaction(Pawn pawn) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction) + return pawn.IsFreeColonist; + + return pawn.Faction != null && + pawn.Faction.IsPlayer && + pawn.RaceProps.Humanlike && + (!pawn.IsSlave || pawn.guest.SlaveIsSecure) && + !pawn.IsSubhuman && + pawn.HostFaction == null; + } + +} + +[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.RemoveGravshipFromMap))] +static class RemoveGravshipFromMap_Patch +{ + static void Prefix(Building_GravEngine engine, out bool __state) + { + __state = false; + if (Multiplayer.Client == null) return; + if (!Multiplayer.GameComp.multifaction) return; + if (engine == null) return; + + engine.Map.PushFaction(engine.Faction); + + __state = true; + } + + static void Finalizer(Building_GravEngine engine, bool __state) + { + if (!__state) return; + engine.Map.PopFaction(); + } +} + +[HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] +static class PlaceGravship_FactionContext_Patch +{ + static void Prefix(Gravship gravship, Map map, out bool __state) + { + __state = false; + if (Multiplayer.Client == null) return; + if (!Multiplayer.GameComp.multifaction) return; + + map.PushFaction(gravship.Faction); + + __state = true; + } + + static void Finalizer(Gravship gravship, Map map, bool __state) + { + if (!__state) return; + map.PopFaction(); + } +} + +[HarmonyPatch(typeof(QuestPart_LendColonistsToFaction), nameof(QuestPart_LendColonistsToFaction.Enable))] +static class QuestPart_LendColonistsToFaction_Enable_Patch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isFreeColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsFreeColonist)); + var replacement = AccessTools.Method(typeof(CompShuttle_ContainedColonistCount_Patch), + nameof(CompShuttle_ContainedColonistCount_Patch.IsFreeColonistAnyPlayerFaction)); + foreach (var ci in insts) + { + if (ci.Calls(isFreeColonist)) + { + ci.opcode = OpCodes.Call; + ci.operand = replacement; + } + yield return ci; + } + } +} + +[HarmonyPatch(typeof(Map), nameof(Map.IsPlayerHome), MethodType.Getter)] +static class Map_IsPlayerHome_Spectator_Patch +{ + static bool Prefix(Map __instance, ref bool __result) + { + if (Multiplayer.Client == null || !Multiplayer.GameComp.multifaction || + Faction.OfPlayer != Multiplayer.WorldComp.spectatorFaction) + return true; + + { + // Is repeat through all player faction a better idea? + if (!__instance.wasSpawnedViaGravShipLanding) + { + MapInfo mapInfo = __instance.info; + if (((mapInfo != null) ? mapInfo.parent : null) == null || __instance.info.parent.Faction?.IsPlayer != true || !__instance.info.parent.def.canBePlayerHome) + { + __result = GravshipUtility.PlayerHasGravEngine(__instance); + return false; + } + } + __result = true; + return false; + } + } +} + +[HarmonyPatch] +static class Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch +{ + static MethodInfo TargetMethod() + { + return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(Pawn_TraderTracker), nameof(Pawn_TraderTracker.ColonyThingsWillingToBuy))); + } + public static List AllBuildingsColonistOfDefOfPlayer(this ListerBuildings lister, ThingDef def) + { + if (def.CanHaveFaction) + return lister.AllBuildingsColonistOfDef(def).FindAll(building => building.Faction == Faction.OfPlayer); + else + return lister.AllBuildingsColonistOfDef(def); + } + public static IEnumerable AllColonistBuildingsOfTypeOfPlayer(this ListerBuildings lister) + { + return lister.AllColonistBuildingsOfType().Where(t => + { + if (t is Building building && (!building.def.CanHaveFaction || building.Faction == Faction.OfPlayer)) + return true; + return false; + }); + } + static IEnumerable Transpiler(IEnumerable insts) + { + var allBuildingsColonistOfDef = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllBuildingsColonistOfDef)); + var allBuildingsColonistOfDefOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllBuildingsColonistOfDefOfPlayer)); + var allColonistBuildingsOfType = AccessTools.Method(typeof(ListerBuildings), nameof(ListerBuildings.AllColonistBuildingsOfType)).MakeGenericMethod([typeof(IHaulSource)]); + var allColonistBuildingsOfTypeOfPlayer = AccessTools.Method(typeof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch), nameof(Pawn_TraderTracker_ColonyThingsWillingToBuy_Patch.AllColonistBuildingsOfTypeOfPlayer)).MakeGenericMethod([typeof(IHaulSource)]); + foreach (var ci in insts) + { + if (ci.Calls(allBuildingsColonistOfDef)) + { + ci.opcode = OpCodes.Call; + ci.operand = allBuildingsColonistOfDefOfPlayer; + } + else if (ci.Calls(allColonistBuildingsOfType)) + { + ci.opcode = OpCodes.Call; + ci.operand = allColonistBuildingsOfTypeOfPlayer; + } + yield return ci; + } + } +} + +[HarmonyPatch] +static class TradeUtility_AllLaunchableThingsForTrade_Patch +{ + static MethodInfo TargetMethod() + { + return AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(TradeUtility), nameof(TradeUtility.AllLaunchableThingsForTrade))); + } + + public static IEnumerable AllPoweredOfPlayer(Map map) + { + return Building_OrbitalTradeBeacon.AllPowered(map).Where(beacon => beacon.Faction == Faction.OfPlayer); + } + static IEnumerable Transpiler(IEnumerable insts) + { + var allPowered = AccessTools.Method(typeof(Building_OrbitalTradeBeacon), nameof(Building_OrbitalTradeBeacon.AllPowered)); + var allPoweredOfPlayer = AccessTools.Method(typeof(TradeUtility_AllLaunchableThingsForTrade_Patch), nameof(TradeUtility_AllLaunchableThingsForTrade_Patch.AllPoweredOfPlayer)); + foreach (var ci in insts) + { + if (ci.Calls(allPowered)) + { + ci.opcode = OpCodes.Call; + ci.operand = allPoweredOfPlayer; + } + + yield return ci; + } + } +} diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index d770b9c36..c4ccd2794 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -1,258 +1,258 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using HarmonyLib; -using Multiplayer.Client.AsyncTime; -using Multiplayer.Client.Comp; -using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Patches; -using Multiplayer.Client.Util; -using Multiplayer.Common; -using RimWorld; -using UnityEngine; -using Verse; -#if EDIT_COMPILE_RELOAD -using HotSwap; -#endif - -namespace Multiplayer.Client -{ - public static class Multiplayer - { - public const string WebsiteLink = "https://rimworldmultiplayer.com"; - // There is also a link in About.xml. Remember to update both - public const string DiscordLink = "https://discord.gg/S4bxXpv"; - - public static Harmony harmony = new("multiplayer"); - public static MpSettings settings; - - public static MultiplayerGame game; - public static MultiplayerSession session; - - public static Common.SyncSerialization serialization; - - public static MultiplayerServer LocalServer { get; set; } - public static Thread localServerThread; - - public static ConnectionBase Client => session?.client; - public static PacketLogWindow WriterLog => session?.writerLog; - public static PacketLogWindow ReaderLog => session?.readerLog; - public static bool IsReplay => session?.replay ?? false; - - public static string username; - - public static bool reloading; - - public static MultiplayerGameComp GameComp => game.gameComp; - public static MultiplayerWorldComp WorldComp => game.worldComp; - public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; - public static ThingsById ThingsById => game.thingsById; - - public static bool ShowDevInfo => Prefs.DevMode && settings.showDevInfo; - public static bool GhostMode => session is { ghostModeCheckbox: true }; - - public static Faction RealPlayerFaction => Client != null ? game.RealPlayerFaction : Faction.OfPlayer; - - public static bool ExecutingCmds => TickPatch.currentExecutingCmdType != null; - public static bool ExecutingCmdDebugTool => TickPatch.currentExecutingCmdType == CommandType.DebugTools; - public static bool Ticking => AsyncWorldTimeComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; - public static Map MapContext => AsyncTimeComp.tickingMap ?? AsyncTimeComp.executingCmdMap; - - public static bool dontSync; - public static bool ShouldSync => InInterface && !dontSync; - public static bool InInterface => - Client != null - && !Ticking - && !ExecutingCmds - && !reloading - && Current.ProgramState == ProgramState.Playing - && LongEventHandler.currentEvent == null; - - public static string ReplaysDir => GenFilePaths.FolderUnderSaveData("MpReplays"); - public static string DesyncsDir => GenFilePaths.FolderUnderSaveData("MpDesyncs"); - public static string LogsDir => GenFilePaths.FolderUnderSaveData("MpLogs"); - public static string CacheDir => GenFilePaths.FolderUnderSaveData("MpCache"); - - public static Stopwatch clock = Stopwatch.StartNew(); - - public static bool arbiterInstance; - public static bool loadingErrors; - public static Stopwatch harmonyWatch = new(); - - public static ModContentPack modContentPack; - - public static void InitMultiplayer(ModContentPack content) - { -#if EDIT_COMPILE_RELOAD - EcrLog.messageCallback = Log.Message; - EcrLog.errorCallback = Log.Error; - // EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "MultiplayerCommon.dll_orig"); - EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "Multiplayer.dll_orig"); -#endif - - modContentPack = content; - Native.EarlyInit( - Application.platform switch - { - RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, - RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, - RuntimePlatform.OSXEditor => Native.NativeOS.OSX, - RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, - _ => Native.NativeOS.Windows - }); - - DisableOmitFramePointer(); - JittedMethods.Init(); - - MultiplayerLoader.Multiplayer.settingsWindowDrawer = - rect => MpSettingsUI.DoSettingsWindowContents(settings, rect); - - using (DeepProfilerWrapper.Section("Multiplayer CacheTypeHierarchy")) - TypeCache.CacheTypeHierarchy(); - - using (DeepProfilerWrapper.Section("Multiplayer CacheTypeByName")) - TypeCache.CacheTypeByName(); - - if (GenCommandLine.CommandLineArgPassed("profiler")) - { - SimpleProfiler.CheckAvailable(); - Log.Message($"Profiler: {SimpleProfiler.available}"); - SimpleProfiler.Init("prof"); - } - - if (GenCommandLine.CommandLineArgPassed("arbiter")) - { - ArbiterWindowFix.Run(); - arbiterInstance = true; - } - - ScribeLike.provider = new ScribeProvider(); - settings = MultiplayerLoader.Multiplayer.instance!.GetSettings(); - - EarlyInit.ProcessEnvironment(); - EarlyInit.EarlyPatches(harmony); - MpReflection.allAssembliesHook = RwAllAssemblies; - EarlyInit.InitSync(); - CheckInterfaceVersions(); - - LongEventHandler.ExecuteWhenFinished(() => { - // Double Execute ensures it'll run last. - LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); - }); - - MultiplayerData.modCtorRoundMode = RoundMode.GetCurrentRoundMode(); - VersionChecker.Init(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using HarmonyLib; +using Multiplayer.Client.AsyncTime; +using Multiplayer.Client.Comp; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Patches; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; +#if EDIT_COMPILE_RELOAD +using HotSwap; +#endif + +namespace Multiplayer.Client +{ + public static class Multiplayer + { + public const string WebsiteLink = "https://rimworldmultiplayer.com"; + // There is also a link in About.xml. Remember to update both + public const string DiscordLink = "https://discord.gg/S4bxXpv"; + + public static Harmony harmony = new("multiplayer"); + public static MpSettings settings; + + public static MultiplayerGame game; + public static MultiplayerSession session; + + public static Common.SyncSerialization serialization; + + public static MultiplayerServer LocalServer { get; set; } + public static Thread localServerThread; + + public static ConnectionBase Client => session?.client; + public static PacketLogWindow WriterLog => session?.writerLog; + public static PacketLogWindow ReaderLog => session?.readerLog; + public static bool IsReplay => session?.replay ?? false; + + public static string username; + + public static bool reloading; + + public static MultiplayerGameComp GameComp => game.gameComp; + public static MultiplayerWorldComp WorldComp => game.worldComp; + public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; + public static ThingsById ThingsById => game.thingsById; + + public static bool ShowDevInfo => Prefs.DevMode && settings.showDevInfo; + public static bool GhostMode => session is { ghostModeCheckbox: true }; + + public static Faction RealPlayerFaction => Client != null ? game.RealPlayerFaction : Faction.OfPlayer; + + public static bool ExecutingCmds => TickPatch.currentExecutingCmdType != null; + public static bool ExecutingCmdDebugTool => TickPatch.currentExecutingCmdType == CommandType.DebugTools; + public static bool Ticking => AsyncWorldTimeComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; + public static Map MapContext => AsyncTimeComp.tickingMap ?? AsyncTimeComp.executingCmdMap; + + public static bool dontSync; + public static bool ShouldSync => InInterface && !dontSync; + public static bool InInterface => + Client != null + && !Ticking + && !ExecutingCmds + && !reloading + && Current.ProgramState == ProgramState.Playing + && LongEventHandler.currentEvent == null; + + public static string ReplaysDir => GenFilePaths.FolderUnderSaveData("MpReplays"); + public static string DesyncsDir => GenFilePaths.FolderUnderSaveData("MpDesyncs"); + public static string LogsDir => GenFilePaths.FolderUnderSaveData("MpLogs"); + public static string CacheDir => GenFilePaths.FolderUnderSaveData("MpCache"); + + public static Stopwatch clock = Stopwatch.StartNew(); + + public static bool arbiterInstance; + public static bool loadingErrors; + public static Stopwatch harmonyWatch = new(); + + public static ModContentPack modContentPack; + + public static void InitMultiplayer(ModContentPack content) + { +#if EDIT_COMPILE_RELOAD + EcrLog.messageCallback = Log.Message; + EcrLog.errorCallback = Log.Error; + // EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "MultiplayerCommon.dll_orig"); + EditCompileReload.RegisterAssemblyWatcher("Mods/Multiplayer/Source/Client/bin", "Multiplayer.dll_orig"); +#endif + + modContentPack = content; + Native.EarlyInit( + Application.platform switch + { + RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, + RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, + RuntimePlatform.OSXEditor => Native.NativeOS.OSX, + RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, + _ => Native.NativeOS.Windows + }); + + DisableOmitFramePointer(); + JittedMethods.Init(); + + MultiplayerLoader.Multiplayer.settingsWindowDrawer = + rect => MpSettingsUI.DoSettingsWindowContents(settings, rect); + + using (DeepProfilerWrapper.Section("Multiplayer CacheTypeHierarchy")) + TypeCache.CacheTypeHierarchy(); + + using (DeepProfilerWrapper.Section("Multiplayer CacheTypeByName")) + TypeCache.CacheTypeByName(); + + if (GenCommandLine.CommandLineArgPassed("profiler")) + { + SimpleProfiler.CheckAvailable(); + Log.Message($"Profiler: {SimpleProfiler.available}"); + SimpleProfiler.Init("prof"); + } + + if (GenCommandLine.CommandLineArgPassed("arbiter")) + { + ArbiterWindowFix.Run(); + arbiterInstance = true; + } + + ScribeLike.provider = new ScribeProvider(); + settings = MultiplayerLoader.Multiplayer.instance!.GetSettings(); + + EarlyInit.ProcessEnvironment(); + EarlyInit.EarlyPatches(harmony); + MpReflection.allAssembliesHook = RwAllAssemblies; + EarlyInit.InitSync(); + CheckInterfaceVersions(); + + LongEventHandler.ExecuteWhenFinished(() => { + // Double Execute ensures it'll run last. + LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); + }); + + MultiplayerData.modCtorRoundMode = RoundMode.GetCurrentRoundMode(); + VersionChecker.Init(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void DisableOmitFramePointer() { if (!Native.IsMonoRuntime) return; Native.mini_parse_debug_option("disable_omit_fp"); - } - - private static void CheckInterfaceVersions() - { - var mpAssembly = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Multiplayer"); - var curVersion = new Version( - (mpAssembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0] as AssemblyFileVersionAttribute).Version - ); - - Log.Message($"Current MultiplayerAPI version: {curVersion}"); - - foreach (var mod in LoadedModManager.RunningMods) { - if (mod.assemblies.loadedAssemblies.NullOrEmpty()) - continue; - - if (mod == modContentPack) - continue; - - // Test if mod is using multiplayer api - if (!mod.assemblies.loadedAssemblies.Any(a => a.GetName().Name == MpVersion.ApiAssemblyName)) { - continue; - } - - // Retrieve the original dll - var info = MultiplayerData.GetModAssemblies(mod) - .Select(f => FileVersionInfo.GetVersionInfo(f.FullName)) - .FirstOrDefault(v => v.ProductName == "Multiplayer"); - - if (info == null) { - // There are certain mods that don't include the API, namely compat - // Can we test them? - continue; - } - - var version = new Version(info.FileVersion); - - Log.Message($"Mod {mod.Name} has MultiplayerAPI client ({version})"); - - if (curVersion > version) - Log.Warning($"Mod {mod.Name} uses an older API version (mod: {version}, current: {curVersion})"); - else if (curVersion < version) - Log.Error($"Mod {mod.Name} uses a newer API version! (mod: {version}, current: {curVersion})\nMake sure the Multiplayer mod is up to date"); - } - } - - public static void StopMultiplayerAndClearAllWindows() - { - StopMultiplayer(); - MpUI.ClearWindowStack(); - } - - public static void StopMultiplayer() - { - Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); - - OnMainThread.ClearScheduled(); - LongEventHandler.ClearQueuedEvents(); - - if (session != null) - { - session.Stop(); - session = null; - - Prefs.Apply(); - } - - if (LocalServer != null) - { - LocalServer.running = false; - localServerThread?.Join(); - LocalServer.TryStop(); - LocalServer = null; - } - - game?.OnDestroy(); - game = null; - - TickPatch.Reset(); - VTRSync.Reset(); - - Find.WindowStack?.WindowOfType()?.Cleanup(true); - SyncFieldUtil.ClearAllBufferedChanges(); - - if (arbiterInstance) - { - arbiterInstance = false; - Application.Quit(); - } - } - - private static IEnumerable RwAllAssemblies() - { - yield return Assembly.GetAssembly(typeof(Game)); - - foreach (ModContentPack mod in LoadedModManager.RunningMods) - foreach (Assembly assembly in mod.assemblies.loadedAssemblies) - yield return assembly; - - if (Assembly.GetEntryAssembly() != null) - yield return Assembly.GetEntryAssembly(); - } - } -} + } + + private static void CheckInterfaceVersions() + { + var mpAssembly = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Multiplayer"); + var curVersion = new Version( + (mpAssembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0] as AssemblyFileVersionAttribute).Version + ); + + Log.Message($"Current MultiplayerAPI version: {curVersion}"); + + foreach (var mod in LoadedModManager.RunningMods) { + if (mod.assemblies.loadedAssemblies.NullOrEmpty()) + continue; + + if (mod == modContentPack) + continue; + + // Test if mod is using multiplayer api + if (!mod.assemblies.loadedAssemblies.Any(a => a.GetName().Name == MpVersion.ApiAssemblyName)) { + continue; + } + + // Retrieve the original dll + var info = MultiplayerData.GetModAssemblies(mod) + .Select(f => FileVersionInfo.GetVersionInfo(f.FullName)) + .FirstOrDefault(v => v.ProductName == "Multiplayer"); + + if (info == null) { + // There are certain mods that don't include the API, namely compat + // Can we test them? + continue; + } + + var version = new Version(info.FileVersion); + + Log.Message($"Mod {mod.Name} has MultiplayerAPI client ({version})"); + + if (curVersion > version) + Log.Warning($"Mod {mod.Name} uses an older API version (mod: {version}, current: {curVersion})"); + else if (curVersion < version) + Log.Error($"Mod {mod.Name} uses a newer API version! (mod: {version}, current: {curVersion})\nMake sure the Multiplayer mod is up to date"); + } + } + + public static void StopMultiplayerAndClearAllWindows() + { + StopMultiplayer(); + MpUI.ClearWindowStack(); + } + + public static void StopMultiplayer() + { + Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); + + OnMainThread.ClearScheduled(); + LongEventHandler.ClearQueuedEvents(); + + if (session != null) + { + session.Stop(); + session = null; + + Prefs.Apply(); + } + + if (LocalServer != null) + { + LocalServer.running = false; + localServerThread?.Join(); + LocalServer.TryStop(); + LocalServer = null; + } + + game?.OnDestroy(); + game = null; + + TickPatch.Reset(); + VTRSync.Reset(); + + Find.WindowStack?.WindowOfType()?.Cleanup(true); + SyncFieldUtil.ClearAllBufferedChanges(); + + if (arbiterInstance) + { + arbiterInstance = false; + Application.Quit(); + } + } + + private static IEnumerable RwAllAssemblies() + { + yield return Assembly.GetAssembly(typeof(Game)); + + foreach (ModContentPack mod in LoadedModManager.RunningMods) + foreach (Assembly assembly in mod.assemblies.loadedAssemblies) + yield return assembly; + + if (Assembly.GetEntryAssembly() != null) + yield return Assembly.GetEntryAssembly(); + } + } +} diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index b021d98dd..8f38f6a00 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -1,85 +1,85 @@ - - - + + + net48 - net10.0 - true - 12 - false - bin - false - false - 0.6.2 - false - Multiplayer.Client - Multiplayer - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\ - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - - - - - - - - - - - - - - - - - + net10.0 + true + 12 + false + bin + false + false + 0.6.2 + false + Multiplayer.Client + Multiplayer + + + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\ + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + + + + + + + + + + + + + + + + + diff --git a/Source/Client/Patches/GravshipTravelSessionPatches.cs b/Source/Client/Patches/GravshipTravelSessionPatches.cs index 8b5e84b4f..82cabdb10 100644 --- a/Source/Client/Patches/GravshipTravelSessionPatches.cs +++ b/Source/Client/Patches/GravshipTravelSessionPatches.cs @@ -1,258 +1,258 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using HarmonyLib; -using Multiplayer.API; -using Multiplayer.Client.Persistent; -using Multiplayer.Client.Util; -using RimWorld; -using RimWorld.Planet; -using Verse; -using Verse.Sound; - -namespace Multiplayer.Client.Patches -{ - #region Input - - [HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation))] - public static class PatchGravshipPreLaunchConfirmation - { - static void Prefix(Building_GravEngine engine, ref Action launchAction) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.OpenSessionAt(engine.Map.Tile); - } - } - - [HarmonyPatch] - public static class PatchGravshipPreLaunchCancel - { - static MethodBase TargetMethod() - { - return MpMethodUtil.GetLambda(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation), lambdaOrdinal: 4); - } - +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using Multiplayer.API; +using Multiplayer.Client.Persistent; +using Multiplayer.Client.Util; +using RimWorld; +using RimWorld.Planet; +using Verse; +using Verse.Sound; + +namespace Multiplayer.Client.Patches +{ + #region Input + + [HarmonyPatch(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation))] + public static class PatchGravshipPreLaunchConfirmation + { + static void Prefix(Building_GravEngine engine, ref Action launchAction) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.OpenSessionAt(engine.Map.Tile); + } + } + + [HarmonyPatch] + public static class PatchGravshipPreLaunchCancel + { + static MethodBase TargetMethod() + { + return MpMethodUtil.GetLambda(typeof(GravshipUtility), nameof(GravshipUtility.PreLaunchConfirmation), lambdaOrdinal: 4); + } + static void Postfix() - { - if (Multiplayer.Client == null) return; - if (!Multiplayer.ExecutingCmds) return; - - GravshipTravelUtils.CloseSessionAt(Find.CurrentMap.Tile); - GravshipTravelUtils.CloseGravshipPrelaunchDialog(); - } - } - - [HarmonyPatch(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp))] - public static class PatchPilotConsoleStartChoosingDestination - { - static void Postfix(CompPilotConsole __instance, bool launching) - { - if (Multiplayer.Client == null) return; - if (!launching) return; - - GravshipTravelUtils.CloseGravshipPrelaunchDialog(); - GravshipTravelUtils.OpenSessionAt(__instance.engine.Map.Tile); - } - } - - [HarmonyPatch] - static class PatchTilePickerCancelLambda - { - static MethodBase TargetMethod() - { - return MpMethodUtil.GetLambda(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp), lambdaOrdinal: 4); - } - - static void Prefix(bool ___launching, ref bool __state) - { - if (Multiplayer.Client == null || ___launching || Multiplayer.dontSync) - return; - - Multiplayer.dontSync = true; - __state = true; - } - - // TODO: Something in Feedback.cs seems to block the wantedMode switch. - // For now, it's set manually - consider keeping it this way permanently. - static void Finalizer(bool ___launching, PlanetTile ___curTile, bool __state) - { - if (Multiplayer.Client == null) return; - if (__state) Multiplayer.dontSync = false; - if (!Multiplayer.ExecutingCmds || !___launching) return; - - Find.World.renderer.wantedMode = WorldRenderMode.None; - Find.TilePicker.StopTargetingInt(); - GravshipTravelUtils.CloseSessionAt(___curTile); - } - } - - [HarmonyPatch] - public static class PatchGravshipMapArriveMethods - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveExistingMap)); - yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveNewMap)); - } - static void Postfix(Gravship gravship) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.OpenSessionAt(gravship.destinationTile); - } - } - - [HarmonyPatch(typeof(Designator_MoveGravship), nameof(Designator_MoveGravship.DesignateSingleCell))] - public static class PatchGravshipDesignatorDeselectForAllClients - { - static void Prefix() => CancelDesignatorDeselection.EnableCanceling(); - - static void Finalizer() => CancelDesignatorDeselection.DisableCanceling(); - } - - // TODO: Is there a better way to synchronize this method? - [HarmonyPatch(typeof(GravshipLandingMarker), nameof(GravshipLandingMarker.BeginLanding))] - public static class PatchBeginLandingToSyncWithClients - { - static bool Prefix(GravshipLandingMarker __instance) - { - if (Multiplayer.Client == null) return true; - if (Multiplayer.ExecutingCmds) return true; - - SyncBeginLanding(__instance); - - return false; - } - - [SyncMethod] - public static void SyncBeginLanding(GravshipLandingMarker landingMarker) - { - var gravshipController = Find.GravshipController; - - if (landingMarker == null || landingMarker.Tile == null) - { - MpLog.Error($"[MP] SyncConfirmGravshipLanding: Marker [{landingMarker != null}] Tile [{landingMarker?.Tile != null}]."); - return; - } - - gravshipController.landingMarker = landingMarker; - landingMarker.BeginLanding(gravshipController); - gravshipController.landingMarker = null; - - if (!TickPatch.currentExecutingCmdIssuedBySelf) - SoundDefOf.Gravship_Land.PlayOneShotOnCamera(); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.AbortLanding))] - public static class PatchAbortLandingToCloseSession - { - static void Prefix(WorldComponent_GravshipController __instance, ref bool __state) - { - if (Multiplayer.Client == null) return; - if (!Multiplayer.ExecutingCmds) return; - - GravshipTravelUtils.CloseSessionAt(__instance.landingTile); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] - public static class PatchPlaceGravshipToUpdateSystemsAfterTheySpawn - { - static void Postfix(Map map) - { - if (Multiplayer.Client == null) return; - - map.glowGrid.GlowGridUpdate_First(); - } - } - - #endregion - - #region Landing/Takeoff freeze - - [HarmonyPatch] - public static class PatchGravshipCutsceneToFreeze - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateTakeoff)); - yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateLanding)); - } - - static void Postfix() - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.StartFreeze(); - } - } - - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.TakeoffEnded))] - public static class PatchGravshipTakeoffEnded - { - static void Prefix(WorldComponent_GravshipController __instance) - { - if (Multiplayer.Client == null) return; - - GravshipTravelUtils.StopFreeze(); - GravshipTravelUtils.CloseSessionAt(__instance.takeoffTile); - } - } - - // TODO: Is the random pushing here necessary? This might be related to issue #638. - [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.LandingEnded))] - public static class PatchGravshipLandingEnded - { - static void Prefix(WorldComponent_GravshipController __instance) - { - if (Multiplayer.Client == null) return; - - Rand.PushState(); - Rand.StateCompressed = __instance.map.AsyncTime().randState; - - GravshipTravelUtils.StopFreeze(); - GravshipTravelUtils.CloseSessionAt(__instance.gravship.destinationTile); - - // LandingEnded calls Find.TickManager.CurTimeSpeed = TimeSpeed.Normal, but that doesn't work in MP as we - // have our own way of changing the map speed. - __instance.map.AsyncTime().DesiredTimeSpeed = TimeSpeed.Normal; - } - - static void Finalizer() - { - if (Multiplayer.Client == null) return; - Rand.PopState(); - } - } - #endregion - - // TODO: Check what this actually does and whether it’s still necessary - // Stop the landing co message from showing every game tick - [HarmonyPatch(typeof(TickManager), nameof(TickManager.PlayerCanControl), MethodType.Getter)] - public static class PatchTickmanagerPlayerCanControlGetter - { - private static bool shownLandingMessage = false; - - static bool Prefix(ref AcceptanceReport __result) - { - if (Multiplayer.Client == null) return true; - - WorldComponent_GravshipController gravshipController = Find.GravshipController; - if (gravshipController != null && gravshipController.LandingAreaConfirmationInProgress) - { - __result = shownLandingMessage ? false : "MessageConfirmLandingAreaFirst".Translate(); - shownLandingMessage = true; - return false; - } - - // Not in a landing session, use vanilla logic for player control - __result = Current.Game.PlayerHasControl; - return false; - } - - // Call this when the landing session ends (e.g., in your session cleanup) - public static void ResetLandingMessageFlag() => shownLandingMessage = false; - } -} + { + if (Multiplayer.Client == null) return; + if (!Multiplayer.ExecutingCmds) return; + + GravshipTravelUtils.CloseSessionAt(Find.CurrentMap.Tile); + GravshipTravelUtils.CloseGravshipPrelaunchDialog(); + } + } + + [HarmonyPatch(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp))] + public static class PatchPilotConsoleStartChoosingDestination + { + static void Postfix(CompPilotConsole __instance, bool launching) + { + if (Multiplayer.Client == null) return; + if (!launching) return; + + GravshipTravelUtils.CloseGravshipPrelaunchDialog(); + GravshipTravelUtils.OpenSessionAt(__instance.engine.Map.Tile); + } + } + + [HarmonyPatch] + static class PatchTilePickerCancelLambda + { + static MethodBase TargetMethod() + { + return MpMethodUtil.GetLambda(typeof(CompPilotConsole), nameof(CompPilotConsole.StartChoosingDestination_NewTemp), lambdaOrdinal: 4); + } + + static void Prefix(bool ___launching, ref bool __state) + { + if (Multiplayer.Client == null || ___launching || Multiplayer.dontSync) + return; + + Multiplayer.dontSync = true; + __state = true; + } + + // TODO: Something in Feedback.cs seems to block the wantedMode switch. + // For now, it's set manually - consider keeping it this way permanently. + static void Finalizer(bool ___launching, PlanetTile ___curTile, bool __state) + { + if (Multiplayer.Client == null) return; + if (__state) Multiplayer.dontSync = false; + if (!Multiplayer.ExecutingCmds || !___launching) return; + + Find.World.renderer.wantedMode = WorldRenderMode.None; + Find.TilePicker.StopTargetingInt(); + GravshipTravelUtils.CloseSessionAt(___curTile); + } + } + + [HarmonyPatch] + public static class PatchGravshipMapArriveMethods + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveExistingMap)); + yield return AccessTools.Method(typeof(GravshipUtility), nameof(GravshipUtility.ArriveNewMap)); + } + static void Postfix(Gravship gravship) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.OpenSessionAt(gravship.destinationTile); + } + } + + [HarmonyPatch(typeof(Designator_MoveGravship), nameof(Designator_MoveGravship.DesignateSingleCell))] + public static class PatchGravshipDesignatorDeselectForAllClients + { + static void Prefix() => CancelDesignatorDeselection.EnableCanceling(); + + static void Finalizer() => CancelDesignatorDeselection.DisableCanceling(); + } + + // TODO: Is there a better way to synchronize this method? + [HarmonyPatch(typeof(GravshipLandingMarker), nameof(GravshipLandingMarker.BeginLanding))] + public static class PatchBeginLandingToSyncWithClients + { + static bool Prefix(GravshipLandingMarker __instance) + { + if (Multiplayer.Client == null) return true; + if (Multiplayer.ExecutingCmds) return true; + + SyncBeginLanding(__instance); + + return false; + } + + [SyncMethod] + public static void SyncBeginLanding(GravshipLandingMarker landingMarker) + { + var gravshipController = Find.GravshipController; + + if (landingMarker == null || landingMarker.Tile == null) + { + MpLog.Error($"[MP] SyncConfirmGravshipLanding: Marker [{landingMarker != null}] Tile [{landingMarker?.Tile != null}]."); + return; + } + + gravshipController.landingMarker = landingMarker; + landingMarker.BeginLanding(gravshipController); + gravshipController.landingMarker = null; + + if (!TickPatch.currentExecutingCmdIssuedBySelf) + SoundDefOf.Gravship_Land.PlayOneShotOnCamera(); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.AbortLanding))] + public static class PatchAbortLandingToCloseSession + { + static void Prefix(WorldComponent_GravshipController __instance, ref bool __state) + { + if (Multiplayer.Client == null) return; + if (!Multiplayer.ExecutingCmds) return; + + GravshipTravelUtils.CloseSessionAt(__instance.landingTile); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.PlaceGravship))] + public static class PatchPlaceGravshipToUpdateSystemsAfterTheySpawn + { + static void Postfix(Map map) + { + if (Multiplayer.Client == null) return; + + map.glowGrid.GlowGridUpdate_First(); + } + } + + #endregion + + #region Landing/Takeoff freeze + + [HarmonyPatch] + public static class PatchGravshipCutsceneToFreeze + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateTakeoff)); + yield return AccessTools.Method(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.InitiateLanding)); + } + + static void Postfix() + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.StartFreeze(); + } + } + + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.TakeoffEnded))] + public static class PatchGravshipTakeoffEnded + { + static void Prefix(WorldComponent_GravshipController __instance) + { + if (Multiplayer.Client == null) return; + + GravshipTravelUtils.StopFreeze(); + GravshipTravelUtils.CloseSessionAt(__instance.takeoffTile); + } + } + + // TODO: Is the random pushing here necessary? This might be related to issue #638. + [HarmonyPatch(typeof(WorldComponent_GravshipController), nameof(WorldComponent_GravshipController.LandingEnded))] + public static class PatchGravshipLandingEnded + { + static void Prefix(WorldComponent_GravshipController __instance) + { + if (Multiplayer.Client == null) return; + + Rand.PushState(); + Rand.StateCompressed = __instance.map.AsyncTime().randState; + + GravshipTravelUtils.StopFreeze(); + GravshipTravelUtils.CloseSessionAt(__instance.gravship.destinationTile); + + // LandingEnded calls Find.TickManager.CurTimeSpeed = TimeSpeed.Normal, but that doesn't work in MP as we + // have our own way of changing the map speed. + __instance.map.AsyncTime().DesiredTimeSpeed = TimeSpeed.Normal; + } + + static void Finalizer() + { + if (Multiplayer.Client == null) return; + Rand.PopState(); + } + } + #endregion + + // TODO: Check what this actually does and whether it’s still necessary + // Stop the landing co message from showing every game tick + [HarmonyPatch(typeof(TickManager), nameof(TickManager.PlayerCanControl), MethodType.Getter)] + public static class PatchTickmanagerPlayerCanControlGetter + { + private static bool shownLandingMessage = false; + + static bool Prefix(ref AcceptanceReport __result) + { + if (Multiplayer.Client == null) return true; + + WorldComponent_GravshipController gravshipController = Find.GravshipController; + if (gravshipController != null && gravshipController.LandingAreaConfirmationInProgress) + { + __result = shownLandingMessage ? false : "MessageConfirmLandingAreaFirst".Translate(); + shownLandingMessage = true; + return false; + } + + // Not in a landing session, use vanilla logic for player control + __result = Current.Game.PlayerHasControl; + return false; + } + + // Call this when the landing session ends (e.g., in your session cleanup) + public static void ResetLandingMessageFlag() => shownLandingMessage = false; + } +} diff --git a/Source/Client/Patches/VanillaTweaks.cs b/Source/Client/Patches/VanillaTweaks.cs index 25292b70e..d8716df64 100644 --- a/Source/Client/Patches/VanillaTweaks.cs +++ b/Source/Client/Patches/VanillaTweaks.cs @@ -1,151 +1,151 @@ -using HarmonyLib; -using RimWorld; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client -{ - // Fixes a lag spike when opening debug tools - [HarmonyPatch(typeof(UIRoot))] - [HarmonyPatch(nameof(UIRoot.UIRootOnGUI))] - static class UIRootPatch - { - static bool done; - - static void Prefix() - { - if (done) return; +using HarmonyLib; +using RimWorld; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client +{ + // Fixes a lag spike when opening debug tools + [HarmonyPatch(typeof(UIRoot))] + [HarmonyPatch(nameof(UIRoot.UIRootOnGUI))] + static class UIRootPatch + { + static bool done; + + static void Prefix() + { + if (done) return; GUI.skin.font = Text.fontStyles[1].font; #if !RIMWORLD_1_7 Text.fontStyles[1].font.fontNames = new[] { "arial", "arialbd", "ariali", "arialbi" }; #endif - done = true; - } - } - - // Fix window focus handling - [HarmonyPatch(typeof(WindowStack))] - [HarmonyPatch(nameof(WindowStack.CloseWindowsBecauseClicked))] - public static class WindowFocusPatch - { - static void Prefix(WindowStack __instance, Window clickedWindow) - { - for (int i = Find.WindowStack.Windows.Count - 1; i >= 0; i--) - { - Window window = Find.WindowStack.Windows[i]; - __instance.focusedWindow = window; - - if (window == clickedWindow || window.closeOnClickedOutside) return; - UI.UnfocusCurrentControl(); - } - - __instance.focusedWindow = null; - } - } - - [HarmonyPatch(typeof(IncidentWorker_PawnsArrive), nameof(IncidentWorker_PawnsArrive.FactionCanBeGroupSource))] - static class FactionCanBeGroupSourcePatch - { - static void Postfix(Faction f, ref bool __result) - { - __result &= f.def.pawnGroupMakers?.Count > 0; - } - } - - [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.AddNewImmediateWindow))] - static class LongEventWindowPreventCameraMotion - { - public const int LongEventWindowId = 62893994; - - // ReSharper disable once InconsistentNaming - static void Postfix(int ID) - { - if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) - { - var window = Find.WindowStack.windows.Find(w => w.ID == ID); - - window.absorbInputAroundWindow = true; - window.preventCameraMotion = true; - } - } - } - - [HarmonyPatch(typeof(Window), nameof(Window.WindowOnGUI))] - static class WindowDrawDarkBackground - { - static void Prefix(Window __instance) - { - if (Current.ProgramState == ProgramState.Entry) return; - - if (__instance.ID == -LongEventWindowPreventCameraMotion.LongEventWindowId || - __instance.ID == -IngameModal.ModalWindowId || - __instance is DisconnectedWindow || - __instance is CaravanFormingProxy - ) - Widgets.DrawBoxSolid(new Rect(0, 0, UI.screenWidth, UI.screenHeight), new Color(0, 0, 0, 0.5f)); - } - } - - // Fixes a bug with long event handler's immediate window draw order - [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.ImmediateWindow))] - static class AddImmediateWindowsDuringLayouting - { - static IEnumerable Transpiler(IEnumerable insts) - { - bool found = false; - - foreach (var inst in insts) - { - if (!found && inst.opcode == OpCodes.Ldc_I4_7) - { - yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AddImmediateWindowsDuringLayouting), nameof(Process))); - found = true; - } - - yield return inst; - } - } - - static EventType Process(EventType type) => type == EventType.Layout ? EventType.Repaint : type; - } - - [HarmonyPatch(typeof(Widgets), nameof(Widgets.DrawLine))] - static class DrawLineOnlyOnRepaint - { - static bool Prefix() => Event.current.type == EventType.Repaint; - } - - [HarmonyPatch] - public static class WidgetsResolveParsePatch - { - static MethodBase TargetMethod() - { - return AccessTools.Method(typeof(Widgets), nameof(Widgets.ResolveParseNow)).MakeGenericMethod(typeof(int)); - } - - // Fix input field handling - static void Prefix(bool force, ref int val, ref string buffer, ref string edited) - { - if (force) - edited = Widgets.ToStringTypedIn(val); - } - } - - // Fix a lag spike when spawning/destroying things which affect the exit grid - [HarmonyPatch(typeof(ExitMapGrid), nameof(ExitMapGrid.Grid), MethodType.Getter)] - static class ExitMapGridPatch - { - static bool Prefix(ExitMapGrid __instance, ref BoolGrid __result) - { - // A copy of the getter without a costly call to MapUsesExitGrid - if (__instance.dirty) - __instance.Rebuild(); - __result = __instance.exitMapGrid; - return false; - } - } - -} + done = true; + } + } + + // Fix window focus handling + [HarmonyPatch(typeof(WindowStack))] + [HarmonyPatch(nameof(WindowStack.CloseWindowsBecauseClicked))] + public static class WindowFocusPatch + { + static void Prefix(WindowStack __instance, Window clickedWindow) + { + for (int i = Find.WindowStack.Windows.Count - 1; i >= 0; i--) + { + Window window = Find.WindowStack.Windows[i]; + __instance.focusedWindow = window; + + if (window == clickedWindow || window.closeOnClickedOutside) return; + UI.UnfocusCurrentControl(); + } + + __instance.focusedWindow = null; + } + } + + [HarmonyPatch(typeof(IncidentWorker_PawnsArrive), nameof(IncidentWorker_PawnsArrive.FactionCanBeGroupSource))] + static class FactionCanBeGroupSourcePatch + { + static void Postfix(Faction f, ref bool __result) + { + __result &= f.def.pawnGroupMakers?.Count > 0; + } + } + + [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.AddNewImmediateWindow))] + static class LongEventWindowPreventCameraMotion + { + public const int LongEventWindowId = 62893994; + + // ReSharper disable once InconsistentNaming + static void Postfix(int ID) + { + if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) + { + var window = Find.WindowStack.windows.Find(w => w.ID == ID); + + window.absorbInputAroundWindow = true; + window.preventCameraMotion = true; + } + } + } + + [HarmonyPatch(typeof(Window), nameof(Window.WindowOnGUI))] + static class WindowDrawDarkBackground + { + static void Prefix(Window __instance) + { + if (Current.ProgramState == ProgramState.Entry) return; + + if (__instance.ID == -LongEventWindowPreventCameraMotion.LongEventWindowId || + __instance.ID == -IngameModal.ModalWindowId || + __instance is DisconnectedWindow || + __instance is CaravanFormingProxy + ) + Widgets.DrawBoxSolid(new Rect(0, 0, UI.screenWidth, UI.screenHeight), new Color(0, 0, 0, 0.5f)); + } + } + + // Fixes a bug with long event handler's immediate window draw order + [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.ImmediateWindow))] + static class AddImmediateWindowsDuringLayouting + { + static IEnumerable Transpiler(IEnumerable insts) + { + bool found = false; + + foreach (var inst in insts) + { + if (!found && inst.opcode == OpCodes.Ldc_I4_7) + { + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AddImmediateWindowsDuringLayouting), nameof(Process))); + found = true; + } + + yield return inst; + } + } + + static EventType Process(EventType type) => type == EventType.Layout ? EventType.Repaint : type; + } + + [HarmonyPatch(typeof(Widgets), nameof(Widgets.DrawLine))] + static class DrawLineOnlyOnRepaint + { + static bool Prefix() => Event.current.type == EventType.Repaint; + } + + [HarmonyPatch] + public static class WidgetsResolveParsePatch + { + static MethodBase TargetMethod() + { + return AccessTools.Method(typeof(Widgets), nameof(Widgets.ResolveParseNow)).MakeGenericMethod(typeof(int)); + } + + // Fix input field handling + static void Prefix(bool force, ref int val, ref string buffer, ref string edited) + { + if (force) + edited = Widgets.ToStringTypedIn(val); + } + } + + // Fix a lag spike when spawning/destroying things which affect the exit grid + [HarmonyPatch(typeof(ExitMapGrid), nameof(ExitMapGrid.Grid), MethodType.Getter)] + static class ExitMapGridPatch + { + static bool Prefix(ExitMapGrid __instance, ref BoolGrid __result) + { + // A copy of the getter without a costly call to MapUsesExitGrid + if (__instance.dirty) + __instance.Rebuild(); + __result = __instance.exitMapGrid; + return false; + } + } + +} diff --git a/Source/Client/Syncing/SyncTemplates.cs b/Source/Client/Syncing/SyncTemplates.cs index c01410980..079ae13fc 100644 --- a/Source/Client/Syncing/SyncTemplates.cs +++ b/Source/Client/Syncing/SyncTemplates.cs @@ -1,85 +1,85 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; - -namespace Multiplayer.Client -{ - public static class SyncTemplates - { - public static HarmonyMethod CreateTranspiler() => new HarmonyMethod(m_Transpiler) { - priority = Priority.First - }; - - static bool General(int internalId, object instance, object[] args) - { - if (Multiplayer.ShouldSync) { - Sync.internalIdToSyncMethod[internalId].DoSync(instance, args); - return false; - } - - return true; - } - - static readonly MethodInfo m_General = SymbolExtensions.GetMethodInfo(() => General(0, null, Array.Empty())); - static readonly MethodInfo m_Transpiler = SymbolExtensions.GetMethodInfo(() => Transpiler(null, null, null)); - - static IEnumerable Transpiler(MethodBase original, IEnumerable instructions, ILGenerator gen) - { - int idx; - var label = gen.DefineLabel(); - var parameters = original.GetParameters(); - - idx = 0; - foreach (var pInfo in parameters) { - var argIndex = idx++ + (original.IsStatic ? 0 : 1); - var pType = pInfo.ParameterType; - if (pInfo.IsOut) { +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace Multiplayer.Client +{ + public static class SyncTemplates + { + public static HarmonyMethod CreateTranspiler() => new HarmonyMethod(m_Transpiler) { + priority = Priority.First + }; + + static bool General(int internalId, object instance, object[] args) + { + if (Multiplayer.ShouldSync) { + Sync.internalIdToSyncMethod[internalId].DoSync(instance, args); + return false; + } + + return true; + } + + static readonly MethodInfo m_General = SymbolExtensions.GetMethodInfo(() => General(0, null, Array.Empty())); + static readonly MethodInfo m_Transpiler = SymbolExtensions.GetMethodInfo(() => Transpiler(null, null, null)); + + static IEnumerable Transpiler(MethodBase original, IEnumerable instructions, ILGenerator gen) + { + int idx; + var label = gen.DefineLabel(); + var parameters = original.GetParameters(); + + idx = 0; + foreach (var pInfo in parameters) { + var argIndex = idx++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + if (pInfo.IsOut) { pType = pType.GetElementType(); yield return new CodeInstruction(OpCodes.Ldarg, argIndex); yield return new CodeInstruction(OpCodes.Initobj, pType); - } - } - - yield return new CodeInstruction(OpCodes.Ldc_I4, Sync.methodBaseToInternalId[original]); - yield return new CodeInstruction(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0); - - yield return new CodeInstruction(OpCodes.Ldc_I4, parameters.Length); - yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); - - idx = 0; - var arrayIdx = 0; - foreach (var pInfo in parameters) { - var argIndex = idx++ + (original.IsStatic ? 0 : 1); - var pType = pInfo.ParameterType; - yield return new CodeInstruction(OpCodes.Dup); - yield return new CodeInstruction(OpCodes.Ldc_I4, arrayIdx++); - yield return new CodeInstruction(OpCodes.Ldarg, argIndex); - if (pType.IsByRef) { - pType = pType.GetElementType(); - if (pType.IsValueType) { - yield return new CodeInstruction(OpCodes.Ldobj, pType); - yield return new CodeInstruction(OpCodes.Box, pType); - } else - yield return new CodeInstruction(OpCodes.Ldind_Ref, pType); - } else if (pType.IsValueType) - yield return new CodeInstruction(OpCodes.Box, pType); - yield return new CodeInstruction(OpCodes.Stelem_Ref); - } - yield return new CodeInstruction(OpCodes.Call, m_General); - yield return new CodeInstruction(OpCodes.Brtrue, label); - foreach (var code in CreateDefaultCodes(gen, AccessTools.GetReturnedType(original))) - yield return code; - yield return new CodeInstruction(OpCodes.Ret); - - var list = instructions.ToList(); - list.First().labels.Add(label); - foreach (var instruction in list) - yield return instruction; - } - + } + } + + yield return new CodeInstruction(OpCodes.Ldc_I4, Sync.methodBaseToInternalId[original]); + yield return new CodeInstruction(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0); + + yield return new CodeInstruction(OpCodes.Ldc_I4, parameters.Length); + yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); + + idx = 0; + var arrayIdx = 0; + foreach (var pInfo in parameters) { + var argIndex = idx++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + yield return new CodeInstruction(OpCodes.Dup); + yield return new CodeInstruction(OpCodes.Ldc_I4, arrayIdx++); + yield return new CodeInstruction(OpCodes.Ldarg, argIndex); + if (pType.IsByRef) { + pType = pType.GetElementType(); + if (pType.IsValueType) { + yield return new CodeInstruction(OpCodes.Ldobj, pType); + yield return new CodeInstruction(OpCodes.Box, pType); + } else + yield return new CodeInstruction(OpCodes.Ldind_Ref, pType); + } else if (pType.IsValueType) + yield return new CodeInstruction(OpCodes.Box, pType); + yield return new CodeInstruction(OpCodes.Stelem_Ref); + } + yield return new CodeInstruction(OpCodes.Call, m_General); + yield return new CodeInstruction(OpCodes.Brtrue, label); + foreach (var code in CreateDefaultCodes(gen, AccessTools.GetReturnedType(original))) + yield return code; + yield return new CodeInstruction(OpCodes.Ret); + + var list = instructions.ToList(); + list.First().labels.Add(label); + foreach (var instruction in list) + yield return instruction; + } + static IEnumerable CreateDefaultCodes(ILGenerator generator, Type type) { if (type == typeof(void)) yield break; @@ -87,17 +87,17 @@ static IEnumerable CreateDefaultCodes(ILGenerator generator, Ty type = type.GetElementType(); if (!type.IsValueType) { - yield return new CodeInstruction(OpCodes.Ldnull); - yield break; - } + yield return new CodeInstruction(OpCodes.Ldnull); + yield break; + } if (type.IsValueType) { - var v = generator.DeclareLocal(type); - yield return new CodeInstruction(OpCodes.Ldloca, v); + var v = generator.DeclareLocal(type); + yield return new CodeInstruction(OpCodes.Ldloca, v); yield return new CodeInstruction(OpCodes.Initobj, type); yield return new CodeInstruction(OpCodes.Ldloc, v); yield break; } - } - - } -} + } + + } +} diff --git a/Source/Client/UI/MainMenuAnimation.cs b/Source/Client/UI/MainMenuAnimation.cs index 73f7ad27b..c5c14873f 100644 --- a/Source/Client/UI/MainMenuAnimation.cs +++ b/Source/Client/UI/MainMenuAnimation.cs @@ -1,440 +1,440 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using Multiplayer.Client.Util; -using RimWorld; -using UnityEngine; -using Verse; -using Random = System.Random; - -namespace Multiplayer.Client -{ - [HarmonyPatch(typeof(UI_BackgroundMain), nameof(UI_BackgroundMain.DoOverlay))] - [StaticConstructorOnStartup] - static class MainMenuAnimation - { - static Texture2D texture; - - const float r = 2282; // radius of planet - const float cx = r + 80, cy = r + 384; // center coords - const float origWidth = 8000, origHeight = 5000; - const int texWidth = (int)cx + 1871; // max extent in the x direction - const int texHeight = 5000; - - // The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand - // Verse.Rand can't be used here because it isn't thread-safe +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Client.Util; +using RimWorld; +using UnityEngine; +using Verse; +using Random = System.Random; + +namespace Multiplayer.Client +{ + [HarmonyPatch(typeof(UI_BackgroundMain), nameof(UI_BackgroundMain.DoOverlay))] + [StaticConstructorOnStartup] + static class MainMenuAnimation + { + static Texture2D texture; + + const float r = 2282; // radius of planet + const float cx = r + 80, cy = r + 384; // center coords + const float origWidth = 8000, origHeight = 5000; + const int texWidth = (int)cx + 1871; // max extent in the x direction + const int texHeight = 5000; + + // The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand + // Verse.Rand can't be used here because it isn't thread-safe // Cosmetic only: no dependency on Unity's hardware identifier. private static Random rand = new(); - - private static float newPulseTimer; - - static MainMenuAnimation() - { - using (DeepProfilerWrapper.Section("Multiplayer main menu animation texture gen")) - { - texture = new(texWidth, texHeight, TextureFormat.RGBA32, false); - RegenTexture(); - } - } - - static void Prefix(UI_BackgroundMain __instance, Rect bgRect) - { - if (Input.GetKeyDown(KeyCode.KeypadPeriod)) - DoMainMenuPatch.hideMainMenu = !DoMainMenuPatch.hideMainMenu; - - if (!Multiplayer.settings.showMainMenuAnim - || __instance.overrideBGImage != null && __instance.overrideBGImage != UI_BackgroundMain.BGPlanet) - return; - - if (Input.GetKeyDown(KeyCode.End)) - RegenTexture(); - - GUI.DrawTexture(bgRect with { width = texWidth * bgRect.width / origWidth }, texture, ScaleMode.ScaleToFit); - -#if DEBUG - if (Input.GetKey(KeyCode.LeftShift)) - DrawEdges(bgRect); -#endif - - if (Event.current.type != EventType.Repaint) return; - - newPulseTimer += Time.deltaTime; - if (newPulseTimer > 1 / 12f) - { - var edge = edges[rand.Next(edges.Count)]; - var switchEndpoints = rand.NextDouble() < 0.5; - pulses.Add(new Pulse(switchEndpoints ? edge.v2 : edge.v1, switchEndpoints ? edge.v1 : edge.v2) { starting = true }); - newPulseTimer = 0; - } - - DrawPulses(bgRect); - } - - static void DrawPulses(Rect bgRect) - { - for (int i = pulses.Count - 1; i >= 0; i--) - { - var pulse = pulses[i]; - pulse.progress += Math.Min(Time.deltaTime, 0.05f) * 0.25f / pulse.dist; - - if (pulse.progress > 1f) - { - pulses.RemoveAt(i); - - if (!pulse.EaseOut) - pulses.Add(new Pulse(pulse.end, pulse.nextEnd) - { - ending = rand.NextDouble() < 0.4f - }); - } - - var start = Quaternion.Euler(pulse.start.theta, pulse.start.phi, 0); - var end = Quaternion.Euler(pulse.end.theta, pulse.end.phi, 0); - - var x = pulse.progress; - - var progress = - pulse.EaseIn && pulse.EaseOut ? x < 0.5 ? 2 * x * x : 1 - (float)Math.Pow(-2 * x + 2, 2) / 2 : - pulse.EaseIn ? x * x : - pulse.EaseOut ? 1 - (1 - x) * (1 - x) : - x; - - float theta = Quaternion.Lerp(start, end, progress).eulerAngles.x; - float phi = Quaternion.Lerp(start, end, progress).eulerAngles.y; - - float posX = cx + r * Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad); - float posY = cy + r * Mathf.Sin(theta * Mathf.Deg2Rad); - - const float alphaEase = 0.3f; - var alpha = - pulse.EaseIn && x < alphaEase ? x / alphaEase : - pulse.EaseOut && x > 1f - alphaEase ? (1 - x) / alphaEase : - 1; - - using (MpStyle.Set(new Color(1, 1, 1, alpha))) - GUI.DrawTexture( - new Rect( - GetPos(bgRect, posX, posY), - new(6, 6) - ), - // After changing the game language, RimWorld destroys and reloads all assets but doesn't - // rerun static constructors, so resource fields reference destroyed resources - // The pulses disappear after changing the language - MultiplayerStatic.Pulse - ); - } - } - - static Vert GetNewEnd(Pulse pulse) - { - Vert newEnd = null; - var attempts = 0; - - while (newEnd == null && attempts < 20) - { - var randEdge = edges[rand.Next(edges.Count)]; - var otherVert = randEdge.OtherVert(pulse.end); - if (otherVert != null && otherVert != pulse.start && otherVert != pulse.end) - newEnd = otherVert; - attempts++; - } - - return newEnd; - } - - const float rotate = 35f; - const float maskTheta = 10; - - static void DrawEdges(Rect bgRect) - { - { - var proj1 = r * Project(maskTheta, -90).RotatedBy(rotate) + new Vector2(cx, cy); - var proj2 = r * Project(maskTheta, 90).RotatedBy(rotate) + new Vector2(cx, cy); - Widgets.DrawLine(GetPos(bgRect, proj1.x, proj1.y), GetPos(bgRect, proj2.x, proj2.y), Color.blue, 2f); - } - - foreach (var e in edges) - { - var start = Quaternion.Euler(e.v1.theta, e.v1.phi, 0); - var end = Quaternion.Euler(e.v2.theta, e.v2.phi, 0); - - float prevX = 0; - float prevY = 0; - - for (float d = 0; d <= 1; d += 0.25f) - { - float theta = Quaternion.Lerp(start, end, d).eulerAngles.x; - float phi = Quaternion.Lerp(start, end, d).eulerAngles.y; - - var proj = Project(theta, phi); - float x = cx + r * proj.x; - float y = cy + r * proj.y; - - if (prevX != 0 && prevY != 0) - { - var lineStart = GetPos(bgRect, prevX, prevY); - var lineEnd = GetPos(bgRect, x, y); - - if (d == 0.5f) - MpUI.Label(new Rect(lineStart, new(100, 100)), $"{e.dist}", GameFont.Tiny); - - Widgets.DrawLine(lineStart, lineEnd, Color.red, 2f); - } - - prevX = x; - prevY = y; - } - } - } - - static float SphereDist(float t1, float p1, float t2, float p2) - { - return Mathf.Acos( - Mathf.Sin(t1 * Mathf.Deg2Rad) * Mathf.Sin(t2 * Mathf.Deg2Rad) - + Mathf.Cos(t1 * Mathf.Deg2Rad) * Mathf.Cos(t2 * Mathf.Deg2Rad) * Mathf.Cos((p2 - p1) * Mathf.Deg2Rad) - ); - } - - private static List verts = new(); - private static List edges = new(); - private static List pulses = new(); - - static void Mst() - { - foreach (var v in verts) - { - v.cost = float.PositiveInfinity; - v.costVert = null; - } - - var q = verts.ToList(); - - while (q.Count > 0) - { - var v = q.MinBy(a => a.cost); - q.Remove(v); - - if (v.costVert != null) - edges.Add(new Edge(v, v.costVert)); - - foreach (var w in q) - { - var dist = SphereDist(v.theta, v.phi, w.theta, w.phi); - if (dist < w.cost) - { - w.cost = dist; - w.costVert = v; - } - } - } - } - - static Vector2 InverseSpherical(float x, float y, float r) - { - var ro = new Vector2(x, y).magnitude; - var c = Mathf.Asin(ro / r); - return new Vector2( - Mathf.Asin(y * Mathf.Sin(c) / ro) * Mathf.Rad2Deg, - Mathf.Atan2(x * Mathf.Sin(c), ro * Mathf.Cos(c)) * Mathf.Rad2Deg - ); - } - - static Vector2 GetPos(Rect rect, float x, float y) - { - return rect.min + new Vector2(rect.width * x / origWidth, rect.height * y / origHeight); - } - - static Vector2 Project(float theta, float phi) - { - return new Vector2( - Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad), - Mathf.Sin(theta * Mathf.Deg2Rad) - ); - } - - static void RegenTexture() - { - using var _ = DeepProfilerWrapper.Section("RegenTexture"); - - verts.Clear(); - - // vaniller cities - verts.Add(new Vert { theta = 60, phi = 34 }); - verts.Add(new Vert { theta = -4, phi = -68 }); - verts.Add(new Vert { theta = -2, phi = -68 }); - verts.Add(new Vert { theta = 4, phi = -50 }); - verts.Add(new Vert { theta = 18, phi = -61 }); - - edges.Clear(); - pulses.Clear(); - - var colors = new Color[texWidth * texHeight]; - - // Cities - for (int i = 0; i < 20;) - { - float cityTheta = rand.Range(maskTheta, 75f); - float cityPhi = rand.Range(-75f, 75f); - - // ugh - var pr = Project(cityTheta, cityPhi).RotatedBy(rotate); - var inv = InverseSpherical(pr.x, pr.y, 1f); - cityTheta = inv.x; - cityPhi = inv.y; - - // Avoid the light side - var cityXY = r * Project(cityTheta, cityPhi) + new Vector2(cx, cy); - const float maskR = 2160; - if ((new Vector2(520 + maskR, 64 + maskR) - cityXY).magnitude < maskR) - continue; - - var blobs = rand.Next(2, 5); - if (rand.NextDouble() < 0.15) blobs += rand.Next(2, 4); - - Vector2? lit = null; - - // Blobs - for (int k = 0; k < blobs; k++) - { - var blobSpan = 2.5f + Math.Max(0, (blobs - 3) * 0.25f); - float theta = cityTheta + rand.Range(-blobSpan, blobSpan); - float phi = cityPhi + rand.Range(-blobSpan, blobSpan); - - // Dots - for (int j = 0; j < 7; j++) - { - var blobSize = j == 0 || rand.NextDouble() < 0.3f ? 15 : 10; - - float dotTheta = rand.Range(-1f, 1f); - float dotPhi = rand.Range(-1f, 1f); - - for (int y = -blobSize; y <= blobSize; y++) - for (int x = -blobSize; x <= blobSize; x++) - if (x * x + y * y < blobSize * blobSize) - { - var proj = Project(theta + dotTheta, phi + dotPhi); - float centerX = cx + r * proj.x; - float centerY = cy + r * proj.y; - - var c = 1 - new Vector2(x,y).magnitude / blobSize; - var fromCenter = 1 - new Vector2(dotTheta, dotPhi).magnitude / 2; - var fromCenter2 = 1 - new Vector2(theta - cityTheta, phi - cityPhi).magnitude / 5; - var a = c * fromCenter * fromCenter2; - - if (j == 0) a *= 3f; - else if (a > 0.4) a *= 2f; - - var pos = new Vector2(centerX, centerY); - var color = new Color(0.93f, 0.75f, 0.55f, a * a * 2); - var pixelX = (int)pos.x + x; - var pixelY = (int)origHeight - (int)pos.y + y; - colors[pixelX + pixelY * texWidth] = color; - - if (color.a >= 1 && lit == null) - { - lit = new Vector2(theta + dotTheta, phi + dotPhi); - } - } - } - } - - if (lit != null) - verts.Add(new Vert { theta = lit.Value.x, phi = lit.Value.y }); - - i++; - } - - using (DeepProfilerWrapper.Section("SetPixels and Apply")) - { - texture.SetPixels(colors); - texture.Apply(); - } - - Mst(); - } - - class Vert - { - public float theta; - public float phi; - - public float cost; - public Vert costVert; - } - - class Edge - { - public readonly Vert v1, v2; - public readonly float dist; - - public Edge(Vert v1, Vert v2) - { - this.v1 = v1; - this.v2 = v2; - - dist = SphereDist(v1.theta, v1.phi, v2.theta, v2.phi); - } - - protected bool Equals(Edge other) - { - return - Equals(v1, other.v1) && Equals(v2, other.v2) - || Equals(v1, other.v2) && Equals(v2, other.v1); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Edge)obj); - } - - public override int GetHashCode() - { - return v1.GetHashCode() * v2.GetHashCode(); - } - - public Vert OtherVert(Vert v) - { - return v1 == v ? v2 : v2 == v ? v1 : null; - } - } - - class Pulse - { - public float progress; - public Vert start; - public Vert end; - public float dist; - - public bool starting; - public bool ending; - public Vert nextEnd; - - public bool EaseIn => starting; - public bool EaseOut => ending || nextEnd == null; - - public Pulse(Vert start, Vert end) - { - this.start = start; - this.end = end; - - dist = SphereDist(start.theta, start.phi, end.theta, end.phi); - nextEnd = GetNewEnd(this); - } - } - } - - [HarmonyPatch(typeof(UIRoot_Entry), nameof(UIRoot_Entry.ShouldDoMainMenu), MethodType.Getter)] - static class DoMainMenuPatch - { - public static bool hideMainMenu; - - static void Postfix(ref bool __result) - { - if (hideMainMenu) - __result = false; - } - } -} + + private static float newPulseTimer; + + static MainMenuAnimation() + { + using (DeepProfilerWrapper.Section("Multiplayer main menu animation texture gen")) + { + texture = new(texWidth, texHeight, TextureFormat.RGBA32, false); + RegenTexture(); + } + } + + static void Prefix(UI_BackgroundMain __instance, Rect bgRect) + { + if (Input.GetKeyDown(KeyCode.KeypadPeriod)) + DoMainMenuPatch.hideMainMenu = !DoMainMenuPatch.hideMainMenu; + + if (!Multiplayer.settings.showMainMenuAnim + || __instance.overrideBGImage != null && __instance.overrideBGImage != UI_BackgroundMain.BGPlanet) + return; + + if (Input.GetKeyDown(KeyCode.End)) + RegenTexture(); + + GUI.DrawTexture(bgRect with { width = texWidth * bgRect.width / origWidth }, texture, ScaleMode.ScaleToFit); + +#if DEBUG + if (Input.GetKey(KeyCode.LeftShift)) + DrawEdges(bgRect); +#endif + + if (Event.current.type != EventType.Repaint) return; + + newPulseTimer += Time.deltaTime; + if (newPulseTimer > 1 / 12f) + { + var edge = edges[rand.Next(edges.Count)]; + var switchEndpoints = rand.NextDouble() < 0.5; + pulses.Add(new Pulse(switchEndpoints ? edge.v2 : edge.v1, switchEndpoints ? edge.v1 : edge.v2) { starting = true }); + newPulseTimer = 0; + } + + DrawPulses(bgRect); + } + + static void DrawPulses(Rect bgRect) + { + for (int i = pulses.Count - 1; i >= 0; i--) + { + var pulse = pulses[i]; + pulse.progress += Math.Min(Time.deltaTime, 0.05f) * 0.25f / pulse.dist; + + if (pulse.progress > 1f) + { + pulses.RemoveAt(i); + + if (!pulse.EaseOut) + pulses.Add(new Pulse(pulse.end, pulse.nextEnd) + { + ending = rand.NextDouble() < 0.4f + }); + } + + var start = Quaternion.Euler(pulse.start.theta, pulse.start.phi, 0); + var end = Quaternion.Euler(pulse.end.theta, pulse.end.phi, 0); + + var x = pulse.progress; + + var progress = + pulse.EaseIn && pulse.EaseOut ? x < 0.5 ? 2 * x * x : 1 - (float)Math.Pow(-2 * x + 2, 2) / 2 : + pulse.EaseIn ? x * x : + pulse.EaseOut ? 1 - (1 - x) * (1 - x) : + x; + + float theta = Quaternion.Lerp(start, end, progress).eulerAngles.x; + float phi = Quaternion.Lerp(start, end, progress).eulerAngles.y; + + float posX = cx + r * Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad); + float posY = cy + r * Mathf.Sin(theta * Mathf.Deg2Rad); + + const float alphaEase = 0.3f; + var alpha = + pulse.EaseIn && x < alphaEase ? x / alphaEase : + pulse.EaseOut && x > 1f - alphaEase ? (1 - x) / alphaEase : + 1; + + using (MpStyle.Set(new Color(1, 1, 1, alpha))) + GUI.DrawTexture( + new Rect( + GetPos(bgRect, posX, posY), + new(6, 6) + ), + // After changing the game language, RimWorld destroys and reloads all assets but doesn't + // rerun static constructors, so resource fields reference destroyed resources + // The pulses disappear after changing the language + MultiplayerStatic.Pulse + ); + } + } + + static Vert GetNewEnd(Pulse pulse) + { + Vert newEnd = null; + var attempts = 0; + + while (newEnd == null && attempts < 20) + { + var randEdge = edges[rand.Next(edges.Count)]; + var otherVert = randEdge.OtherVert(pulse.end); + if (otherVert != null && otherVert != pulse.start && otherVert != pulse.end) + newEnd = otherVert; + attempts++; + } + + return newEnd; + } + + const float rotate = 35f; + const float maskTheta = 10; + + static void DrawEdges(Rect bgRect) + { + { + var proj1 = r * Project(maskTheta, -90).RotatedBy(rotate) + new Vector2(cx, cy); + var proj2 = r * Project(maskTheta, 90).RotatedBy(rotate) + new Vector2(cx, cy); + Widgets.DrawLine(GetPos(bgRect, proj1.x, proj1.y), GetPos(bgRect, proj2.x, proj2.y), Color.blue, 2f); + } + + foreach (var e in edges) + { + var start = Quaternion.Euler(e.v1.theta, e.v1.phi, 0); + var end = Quaternion.Euler(e.v2.theta, e.v2.phi, 0); + + float prevX = 0; + float prevY = 0; + + for (float d = 0; d <= 1; d += 0.25f) + { + float theta = Quaternion.Lerp(start, end, d).eulerAngles.x; + float phi = Quaternion.Lerp(start, end, d).eulerAngles.y; + + var proj = Project(theta, phi); + float x = cx + r * proj.x; + float y = cy + r * proj.y; + + if (prevX != 0 && prevY != 0) + { + var lineStart = GetPos(bgRect, prevX, prevY); + var lineEnd = GetPos(bgRect, x, y); + + if (d == 0.5f) + MpUI.Label(new Rect(lineStart, new(100, 100)), $"{e.dist}", GameFont.Tiny); + + Widgets.DrawLine(lineStart, lineEnd, Color.red, 2f); + } + + prevX = x; + prevY = y; + } + } + } + + static float SphereDist(float t1, float p1, float t2, float p2) + { + return Mathf.Acos( + Mathf.Sin(t1 * Mathf.Deg2Rad) * Mathf.Sin(t2 * Mathf.Deg2Rad) + + Mathf.Cos(t1 * Mathf.Deg2Rad) * Mathf.Cos(t2 * Mathf.Deg2Rad) * Mathf.Cos((p2 - p1) * Mathf.Deg2Rad) + ); + } + + private static List verts = new(); + private static List edges = new(); + private static List pulses = new(); + + static void Mst() + { + foreach (var v in verts) + { + v.cost = float.PositiveInfinity; + v.costVert = null; + } + + var q = verts.ToList(); + + while (q.Count > 0) + { + var v = q.MinBy(a => a.cost); + q.Remove(v); + + if (v.costVert != null) + edges.Add(new Edge(v, v.costVert)); + + foreach (var w in q) + { + var dist = SphereDist(v.theta, v.phi, w.theta, w.phi); + if (dist < w.cost) + { + w.cost = dist; + w.costVert = v; + } + } + } + } + + static Vector2 InverseSpherical(float x, float y, float r) + { + var ro = new Vector2(x, y).magnitude; + var c = Mathf.Asin(ro / r); + return new Vector2( + Mathf.Asin(y * Mathf.Sin(c) / ro) * Mathf.Rad2Deg, + Mathf.Atan2(x * Mathf.Sin(c), ro * Mathf.Cos(c)) * Mathf.Rad2Deg + ); + } + + static Vector2 GetPos(Rect rect, float x, float y) + { + return rect.min + new Vector2(rect.width * x / origWidth, rect.height * y / origHeight); + } + + static Vector2 Project(float theta, float phi) + { + return new Vector2( + Mathf.Cos(theta * Mathf.Deg2Rad) * Mathf.Sin(phi * Mathf.Deg2Rad), + Mathf.Sin(theta * Mathf.Deg2Rad) + ); + } + + static void RegenTexture() + { + using var _ = DeepProfilerWrapper.Section("RegenTexture"); + + verts.Clear(); + + // vaniller cities + verts.Add(new Vert { theta = 60, phi = 34 }); + verts.Add(new Vert { theta = -4, phi = -68 }); + verts.Add(new Vert { theta = -2, phi = -68 }); + verts.Add(new Vert { theta = 4, phi = -50 }); + verts.Add(new Vert { theta = 18, phi = -61 }); + + edges.Clear(); + pulses.Clear(); + + var colors = new Color[texWidth * texHeight]; + + // Cities + for (int i = 0; i < 20;) + { + float cityTheta = rand.Range(maskTheta, 75f); + float cityPhi = rand.Range(-75f, 75f); + + // ugh + var pr = Project(cityTheta, cityPhi).RotatedBy(rotate); + var inv = InverseSpherical(pr.x, pr.y, 1f); + cityTheta = inv.x; + cityPhi = inv.y; + + // Avoid the light side + var cityXY = r * Project(cityTheta, cityPhi) + new Vector2(cx, cy); + const float maskR = 2160; + if ((new Vector2(520 + maskR, 64 + maskR) - cityXY).magnitude < maskR) + continue; + + var blobs = rand.Next(2, 5); + if (rand.NextDouble() < 0.15) blobs += rand.Next(2, 4); + + Vector2? lit = null; + + // Blobs + for (int k = 0; k < blobs; k++) + { + var blobSpan = 2.5f + Math.Max(0, (blobs - 3) * 0.25f); + float theta = cityTheta + rand.Range(-blobSpan, blobSpan); + float phi = cityPhi + rand.Range(-blobSpan, blobSpan); + + // Dots + for (int j = 0; j < 7; j++) + { + var blobSize = j == 0 || rand.NextDouble() < 0.3f ? 15 : 10; + + float dotTheta = rand.Range(-1f, 1f); + float dotPhi = rand.Range(-1f, 1f); + + for (int y = -blobSize; y <= blobSize; y++) + for (int x = -blobSize; x <= blobSize; x++) + if (x * x + y * y < blobSize * blobSize) + { + var proj = Project(theta + dotTheta, phi + dotPhi); + float centerX = cx + r * proj.x; + float centerY = cy + r * proj.y; + + var c = 1 - new Vector2(x,y).magnitude / blobSize; + var fromCenter = 1 - new Vector2(dotTheta, dotPhi).magnitude / 2; + var fromCenter2 = 1 - new Vector2(theta - cityTheta, phi - cityPhi).magnitude / 5; + var a = c * fromCenter * fromCenter2; + + if (j == 0) a *= 3f; + else if (a > 0.4) a *= 2f; + + var pos = new Vector2(centerX, centerY); + var color = new Color(0.93f, 0.75f, 0.55f, a * a * 2); + var pixelX = (int)pos.x + x; + var pixelY = (int)origHeight - (int)pos.y + y; + colors[pixelX + pixelY * texWidth] = color; + + if (color.a >= 1 && lit == null) + { + lit = new Vector2(theta + dotTheta, phi + dotPhi); + } + } + } + } + + if (lit != null) + verts.Add(new Vert { theta = lit.Value.x, phi = lit.Value.y }); + + i++; + } + + using (DeepProfilerWrapper.Section("SetPixels and Apply")) + { + texture.SetPixels(colors); + texture.Apply(); + } + + Mst(); + } + + class Vert + { + public float theta; + public float phi; + + public float cost; + public Vert costVert; + } + + class Edge + { + public readonly Vert v1, v2; + public readonly float dist; + + public Edge(Vert v1, Vert v2) + { + this.v1 = v1; + this.v2 = v2; + + dist = SphereDist(v1.theta, v1.phi, v2.theta, v2.phi); + } + + protected bool Equals(Edge other) + { + return + Equals(v1, other.v1) && Equals(v2, other.v2) + || Equals(v1, other.v2) && Equals(v2, other.v1); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((Edge)obj); + } + + public override int GetHashCode() + { + return v1.GetHashCode() * v2.GetHashCode(); + } + + public Vert OtherVert(Vert v) + { + return v1 == v ? v2 : v2 == v ? v1 : null; + } + } + + class Pulse + { + public float progress; + public Vert start; + public Vert end; + public float dist; + + public bool starting; + public bool ending; + public Vert nextEnd; + + public bool EaseIn => starting; + public bool EaseOut => ending || nextEnd == null; + + public Pulse(Vert start, Vert end) + { + this.start = start; + this.end = end; + + dist = SphereDist(start.theta, start.phi, end.theta, end.phi); + nextEnd = GetNewEnd(this); + } + } + } + + [HarmonyPatch(typeof(UIRoot_Entry), nameof(UIRoot_Entry.ShouldDoMainMenu), MethodType.Getter)] + static class DoMainMenuPatch + { + public static bool hideMainMenu; + + static void Postfix(ref bool __result) + { + if (hideMainMenu) + __result = false; + } + } +} diff --git a/Source/Client/Util/MpUI.cs b/Source/Client/Util/MpUI.cs index f055dde2b..590b34d02 100644 --- a/Source/Client/Util/MpUI.cs +++ b/Source/Client/Util/MpUI.cs @@ -1,278 +1,278 @@ -using System.Collections.Generic; -using HarmonyLib; -using RimWorld; -using UnityEngine; -using Verse; -using Verse.Sound; - -namespace Multiplayer.Client.Util -{ - public static class MpUI - { - public static Vector2 Resolution => new Vector2(UI.screenWidth, UI.screenHeight); - - public static string FixedEllipsis() - { - int num = Mathf.FloorToInt(Time.realtimeSinceStartup) % 3; - return num switch - { - 0 => ". ", - 1 => ".. ", - _ => "..." - }; - } - - public static void DrawRotatedLine(Vector2 center, float length, float width, float angle, Color color) - { - var size = new Vector2(length, width); - var start = center - size / 2f; - Rect screenRect = new Rect(start.x, start.y, length, width); - Matrix4x4 m = Matrix4x4.TRS(center, Quaternion.Euler(0f, 0f, angle), Vector3.one) * Matrix4x4.TRS(-center, Quaternion.identity, Vector3.one); - - GL.PushMatrix(); - GL.MultMatrix(m); - GUI.DrawTexture(screenRect, Widgets.LineTexAA, ScaleMode.StretchToFill, true, 0f, color, 0f, 0f); - GL.PopMatrix(); - } - - public static void Label(Rect rect, string label, GameFont? font = null, TextAnchor? anchor = null, Color? color = null) - { - var prevFont = Text.Font; - var prevAnchor = Text.Anchor; - var prevColor = GUI.color; - - if (font != null) - Text.Font = font.Value; - - if (anchor != null) - Text.Anchor = anchor.Value; - - if (color != null) - GUI.color = color.Value; - - Widgets.Label(rect, label); - - GUI.color = prevColor; - Text.Anchor = prevAnchor; - Text.Font = prevFont; - } - - public static void Checkbox(float x, float y, ref bool checkOn, bool disabled, float size) - { - if (!disabled && Widgets.ButtonInvisible(new Rect(x, y, size, size), false)) - { - checkOn = !checkOn; - if (checkOn) - SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); - else - SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); - } - - Widgets.CheckboxDraw(x, y, checkOn, disabled, size); - } - - public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, ElementOrder order = ElementOrder.Apart, float size = 24f) - { - TextAnchor anchor = Text.Anchor; - Text.Anchor = TextAnchor.MiddleLeft; - - if (order == ElementOrder.Right) - { - float textWidth = Text.CalcSize(label).x; - rect.x = rect.xMax - textWidth - size - 5f; - rect.width = textWidth + size + 5f; - } - else if (order == ElementOrder.Left) - { - float textWidth = Text.CalcSize(label).x; - rect.width = textWidth + size + 5f; - } - - Widgets.Label(rect, label); - - Checkbox( - rect.xMax - size, - rect.y + (rect.height - size) / 2, - ref checkOn, - disabled, - size); - - Text.Anchor = anchor; - - return rect; - } - - public static string TextEntryLabeled(Rect rect, string label, string text, float labelWidth) - { - Rect labelRect = rect.Rounded(); - labelRect.width = labelWidth; - - using (MpStyle.Set(TextAnchor.MiddleRight)) - Widgets.Label(labelRect, label); - - rect.xMin += labelWidth; - return Widgets.TextField(rect, text); - } - - public static bool TextFieldNumericLabeled(Rect rect, string label, ref T val, ref string buffer, float labelWidth, float min = 0, float max = float.MaxValue, bool clickable = false, string tooltip = null) where T : struct - { - Rect labelRect = rect; - labelRect.width = labelWidth; - - using (MpStyle.Set(TextAnchor.MiddleRight)) - using (MpStyle.Set(WordWrap.NoWrap)) - Widgets.Label(labelRect, label); - - if (clickable) - Widgets.DrawHighlightIfMouseover(labelRect.MinX(labelRect.xMax - Text.CalcSize(label).x - 10)); - - if (tooltip != null) - TooltipHandler.TipRegion(labelRect, tooltip); - - rect.xMin += labelWidth; - Widgets.TextFieldNumeric(rect, ref val, ref buffer, min, max); - - return clickable && Widgets.ButtonInvisible(labelRect); - } - - public static void ClearWindowStack() - { - Find.WindowStack.windows.Clear(); - } - - public static void TryUnfocusCurrentNamedControl(Window window) - { - // Unfocus the input field after a click or ESC press - if ((Event.current.type == EventType.MouseDown || KeyBindingDefOf.Cancel.KeyDownEvent) && !GUI.GetNameOfFocusedControl().NullOrEmpty()) - { - Event.current.Use(); - UI.UnfocusCurrentControl(); - Find.WindowStack.Notify_ManuallySetFocus(window); - } - } - - public static void LabelWithTip(Rect rect, string str, string tip) - { - Widgets.Label(rect, str); - - if (Mouse.IsOver(rect) && tip != null) - TooltipHandler.TipRegion(rect, new TipSignal(tip, 3333)); - } - - public static void LabelTruncatedWithTip(Rect rect, string str, Dictionary cache) - { - var trunc = str.Truncate(rect.width, cache); - LabelWithTip(rect, trunc, trunc != str ? str : null); - } - - public static void CheckboxLabeledWithTip(Rect rect, string label, string tip, ref bool check, bool disabled = false) - { - Widgets.DrawHighlightIfMouseover(rect); - if (tip != null) - TooltipHandler.TipRegion(rect, tip); - CheckboxLabeled(rect, label, ref check, disabled); - } - - public static void CheckboxLabeledWithTipNoHighlight(Rect rect, string label, string tip, ref bool check, bool disabled = false, bool placeTextNearCheckbox = false) - { - if (tip != null) - TooltipHandler.TipRegion(rect, tip); - CheckboxLabeled(rect, label, ref check, disabled, order: placeTextNearCheckbox ? ElementOrder.Right : ElementOrder.Apart); - } - - public static bool ButtonTextWithTip(Rect rect, string label, string tip, bool disabled = false, int? tipId = null) - { - if (tip != null) - TooltipHandler.TipRegion(rect, new TipSignal(tip, tipId ?? tip.GetHashCode())); - - using (MpStyle.Set(TextAnchor.MiddleCenter)) - using (MpStyle.Set(disabled ? Color.grey : Color.white)) - { - var atlas = !disabled && Mouse.IsOver(rect) ? - (Input.GetMouseButton(0) ? Widgets.ButtonBGAtlasClick : Widgets.ButtonBGAtlasMouseover) : - Widgets.ButtonBGAtlas; - - Widgets.DrawAtlas(rect, atlas); - Widgets.Label(rect, label); - } - - var clicked = Widgets.ButtonInvisible(rect, false); - - if (!disabled) - { - MouseoverSounds.DoRegion(rect); - return clicked; - } - - if (clicked) - SoundDefOf.ClickReject.PlayOneShotOnCamera(); - - return false; - } - - public static float LabelFlexibleWidth(Rect rect, string label) - { - var size = Text.CalcSize(label); - rect.width = size.x; - Widgets.Label(rect, label); - return size.x; - } - - public static float LabelFlexibleWidthClickable(Rect rect, string label, ref bool clicked) - { - var size = Text.CalcSize(label); - rect.width = size.x; - Widgets.DrawHighlightIfMouseover(rect); - clicked = Widgets.ButtonInvisible(rect, false); - Widgets.Label(rect, label); - return size.x; - } - - const char PassChar = '\u2022'; - - public static void DoPasswordField(Rect rect, string controlName, ref string password) - { - password ??= ""; - - TextEditor te = - GUI.GetNameOfFocusedControl() == controlName ? - (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl) : - null; - - Vector2 scrollOffset = default; - bool keyDown = Event.current.type == EventType.KeyDown; - if (te != null) - scrollOffset = te.scrollOffset; - - GUI.SetNextControlName(controlName); - - if (Event.current.type is EventType.Repaint or EventType.MouseDown) - GUI.TextField( - rect, - new string(PassChar, password.Length), - Text.textFieldStyles[1] - ); - else - password = GUI.TextField( - rect, - password, - Text.textFieldStyles[1] - ); - - if (te != null && keyDown) - { - te.scrollOffset = scrollOffset; +using System.Collections.Generic; +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace Multiplayer.Client.Util +{ + public static class MpUI + { + public static Vector2 Resolution => new Vector2(UI.screenWidth, UI.screenHeight); + + public static string FixedEllipsis() + { + int num = Mathf.FloorToInt(Time.realtimeSinceStartup) % 3; + return num switch + { + 0 => ". ", + 1 => ".. ", + _ => "..." + }; + } + + public static void DrawRotatedLine(Vector2 center, float length, float width, float angle, Color color) + { + var size = new Vector2(length, width); + var start = center - size / 2f; + Rect screenRect = new Rect(start.x, start.y, length, width); + Matrix4x4 m = Matrix4x4.TRS(center, Quaternion.Euler(0f, 0f, angle), Vector3.one) * Matrix4x4.TRS(-center, Quaternion.identity, Vector3.one); + + GL.PushMatrix(); + GL.MultMatrix(m); + GUI.DrawTexture(screenRect, Widgets.LineTexAA, ScaleMode.StretchToFill, true, 0f, color, 0f, 0f); + GL.PopMatrix(); + } + + public static void Label(Rect rect, string label, GameFont? font = null, TextAnchor? anchor = null, Color? color = null) + { + var prevFont = Text.Font; + var prevAnchor = Text.Anchor; + var prevColor = GUI.color; + + if (font != null) + Text.Font = font.Value; + + if (anchor != null) + Text.Anchor = anchor.Value; + + if (color != null) + GUI.color = color.Value; + + Widgets.Label(rect, label); + + GUI.color = prevColor; + Text.Anchor = prevAnchor; + Text.Font = prevFont; + } + + public static void Checkbox(float x, float y, ref bool checkOn, bool disabled, float size) + { + if (!disabled && Widgets.ButtonInvisible(new Rect(x, y, size, size), false)) + { + checkOn = !checkOn; + if (checkOn) + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); + else + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); + } + + Widgets.CheckboxDraw(x, y, checkOn, disabled, size); + } + + public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, ElementOrder order = ElementOrder.Apart, float size = 24f) + { + TextAnchor anchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + + if (order == ElementOrder.Right) + { + float textWidth = Text.CalcSize(label).x; + rect.x = rect.xMax - textWidth - size - 5f; + rect.width = textWidth + size + 5f; + } + else if (order == ElementOrder.Left) + { + float textWidth = Text.CalcSize(label).x; + rect.width = textWidth + size + 5f; + } + + Widgets.Label(rect, label); + + Checkbox( + rect.xMax - size, + rect.y + (rect.height - size) / 2, + ref checkOn, + disabled, + size); + + Text.Anchor = anchor; + + return rect; + } + + public static string TextEntryLabeled(Rect rect, string label, string text, float labelWidth) + { + Rect labelRect = rect.Rounded(); + labelRect.width = labelWidth; + + using (MpStyle.Set(TextAnchor.MiddleRight)) + Widgets.Label(labelRect, label); + + rect.xMin += labelWidth; + return Widgets.TextField(rect, text); + } + + public static bool TextFieldNumericLabeled(Rect rect, string label, ref T val, ref string buffer, float labelWidth, float min = 0, float max = float.MaxValue, bool clickable = false, string tooltip = null) where T : struct + { + Rect labelRect = rect; + labelRect.width = labelWidth; + + using (MpStyle.Set(TextAnchor.MiddleRight)) + using (MpStyle.Set(WordWrap.NoWrap)) + Widgets.Label(labelRect, label); + + if (clickable) + Widgets.DrawHighlightIfMouseover(labelRect.MinX(labelRect.xMax - Text.CalcSize(label).x - 10)); + + if (tooltip != null) + TooltipHandler.TipRegion(labelRect, tooltip); + + rect.xMin += labelWidth; + Widgets.TextFieldNumeric(rect, ref val, ref buffer, min, max); + + return clickable && Widgets.ButtonInvisible(labelRect); + } + + public static void ClearWindowStack() + { + Find.WindowStack.windows.Clear(); + } + + public static void TryUnfocusCurrentNamedControl(Window window) + { + // Unfocus the input field after a click or ESC press + if ((Event.current.type == EventType.MouseDown || KeyBindingDefOf.Cancel.KeyDownEvent) && !GUI.GetNameOfFocusedControl().NullOrEmpty()) + { + Event.current.Use(); + UI.UnfocusCurrentControl(); + Find.WindowStack.Notify_ManuallySetFocus(window); + } + } + + public static void LabelWithTip(Rect rect, string str, string tip) + { + Widgets.Label(rect, str); + + if (Mouse.IsOver(rect) && tip != null) + TooltipHandler.TipRegion(rect, new TipSignal(tip, 3333)); + } + + public static void LabelTruncatedWithTip(Rect rect, string str, Dictionary cache) + { + var trunc = str.Truncate(rect.width, cache); + LabelWithTip(rect, trunc, trunc != str ? str : null); + } + + public static void CheckboxLabeledWithTip(Rect rect, string label, string tip, ref bool check, bool disabled = false) + { + Widgets.DrawHighlightIfMouseover(rect); + if (tip != null) + TooltipHandler.TipRegion(rect, tip); + CheckboxLabeled(rect, label, ref check, disabled); + } + + public static void CheckboxLabeledWithTipNoHighlight(Rect rect, string label, string tip, ref bool check, bool disabled = false, bool placeTextNearCheckbox = false) + { + if (tip != null) + TooltipHandler.TipRegion(rect, tip); + CheckboxLabeled(rect, label, ref check, disabled, order: placeTextNearCheckbox ? ElementOrder.Right : ElementOrder.Apart); + } + + public static bool ButtonTextWithTip(Rect rect, string label, string tip, bool disabled = false, int? tipId = null) + { + if (tip != null) + TooltipHandler.TipRegion(rect, new TipSignal(tip, tipId ?? tip.GetHashCode())); + + using (MpStyle.Set(TextAnchor.MiddleCenter)) + using (MpStyle.Set(disabled ? Color.grey : Color.white)) + { + var atlas = !disabled && Mouse.IsOver(rect) ? + (Input.GetMouseButton(0) ? Widgets.ButtonBGAtlasClick : Widgets.ButtonBGAtlasMouseover) : + Widgets.ButtonBGAtlas; + + Widgets.DrawAtlas(rect, atlas); + Widgets.Label(rect, label); + } + + var clicked = Widgets.ButtonInvisible(rect, false); + + if (!disabled) + { + MouseoverSounds.DoRegion(rect); + return clicked; + } + + if (clicked) + SoundDefOf.ClickReject.PlayOneShotOnCamera(); + + return false; + } + + public static float LabelFlexibleWidth(Rect rect, string label) + { + var size = Text.CalcSize(label); + rect.width = size.x; + Widgets.Label(rect, label); + return size.x; + } + + public static float LabelFlexibleWidthClickable(Rect rect, string label, ref bool clicked) + { + var size = Text.CalcSize(label); + rect.width = size.x; + Widgets.DrawHighlightIfMouseover(rect); + clicked = Widgets.ButtonInvisible(rect, false); + Widgets.Label(rect, label); + return size.x; + } + + const char PassChar = '\u2022'; + + public static void DoPasswordField(Rect rect, string controlName, ref string password) + { + password ??= ""; + + TextEditor te = + GUI.GetNameOfFocusedControl() == controlName ? + (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl) : + null; + + Vector2 scrollOffset = default; + bool keyDown = Event.current.type == EventType.KeyDown; + if (te != null) + scrollOffset = te.scrollOffset; + + GUI.SetNextControlName(controlName); + + if (Event.current.type is EventType.Repaint or EventType.MouseDown) + GUI.TextField( + rect, + new string(PassChar, password.Length), + Text.textFieldStyles[1] + ); + else + password = GUI.TextField( + rect, + password, + Text.textFieldStyles[1] + ); + + if (te != null && keyDown) + { + te.scrollOffset = scrollOffset; te.text = new string(PassChar, password.Length); #if !RIMWORLD_1_7 AccessTools.Field(typeof(TextEditor), "m_RevealCursor").SetValue(te, true); te.UpdateScrollOffsetIfNeeded(new Event()); #endif - } - } - } - - public enum ElementOrder - { - Apart, Left, Right - } -} + } + } + } + + public enum ElementOrder + { + Apart, Left, Right + } +} diff --git a/Source/Client/Util/MpUtil.cs b/Source/Client/Util/MpUtil.cs index ae7ee5f4b..447ae078c 100644 --- a/Source/Client/Util/MpUtil.cs +++ b/Source/Client/Util/MpUtil.cs @@ -1,206 +1,206 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Serialization; -using System.Text; -using HarmonyLib; -using Verse; - -namespace Multiplayer.Client -{ - public static class MpUtil - { +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text; +using HarmonyLib; +using Verse; + +namespace Multiplayer.Client +{ + public static class MpUtil + { public static unsafe void MarkNoInlining(MethodBase method) { // This offset is a MonoMethod layout, not a CoreCLR MethodDesc. // Harmony handles detouring on CoreCLR; writing here corrupts memory. if (!Native.IsMonoRuntime) return; ushort* iflags = (ushort*)(method.MethodHandle.Value) + 1; - *iflags |= (ushort)MethodImplOptions.NoInlining; - } - - public static object NewObjectNoCtor(Type type) - { - return FormatterServices.GetUninitializedObject(type); - } - - public static T NewObjectNoCtor() - { - return (T)NewObjectNoCtor(typeof(T)); - } - - private static bool ipv6; - private static bool ipv6Checked; - - // Socket.OSSupportsIPv6 doesn't seem to work - public static bool SupportsIPv6() - { - if (!ipv6Checked) - { - try - { - new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - ipv6 = true; - } - catch - { - ipv6 = false; - } - - ipv6Checked = true; - } - - return ipv6; - } - - public static string RwDataFile(string filename) - { - return Path.Combine(GenFilePaths.SaveDataFolderPath, filename); - } - - public static To ShallowCopy(From from, To to) where To : From - { - foreach (var f in AccessTools.GetDeclaredFields(typeof(From))) - if (!f.IsStatic) - f.SetValue(to, f.GetValue(from)); - - return to; - } - - public static string TranslateWithDoubleNewLines(string keyBase, int count) - { - return Enumerable.Range(1, count).Select(n => (keyBase + n).Translate()).Join(delimiter: "\n\n"); - } - - public static string DelegateMethodInfo(MethodBase m) - { - return - m == null - ? "No method" - : $"{m.DeclaringType.DeclaringType?.FullDescription()} {m.DeclaringType.FullDescription()} {m.Name}" - .Replace("<", "[").Replace(">", "]"); - } - - public static MethodBase GetOriginalFromHarmonyReplacement(long replacementAddr) - { - // Todo: this is using a non-public API of Harmony, we should refactor to use https://harmony.pardeike.net/api/HarmonyLib.Harmony.html#HarmonyLib_Harmony_GetOriginalMethod_System_Reflection_MethodInfo_ - // as pardeike suggested in https://github.com/rwmt/Multiplayer/pull/270#issuecomment-1003298289 - return HarmonySharedState.originals - .FirstOrDefault(kv => kv.Key.MethodHandle.GetFunctionPointer().ToInt64() == replacementAddr).Value; - } - - public static string JoinStringsAtMost(this IEnumerable strs, int atMost = 3) - { - return strs.Take(atMost).Join() + (strs.Count() > atMost ? "..." : ""); - } - } - - public struct Container - { - public T Inner { get; } - - public Container(T value) - { - Inner = value; - } - - public static implicit operator Container(T value) - { - return new Container(value); - } - } - - public class UniqueList : IEnumerable - { - private List list = new List(); - private HashSet set = new HashSet(); - - public int Count => list.Count; - public T this[int index] => list[index]; - - public bool Add(T t) - { - if (set.Add(t)) - { - list.Add(t); - return true; - } - - return false; - } - - public T[] ToArray() - { - return list.ToArray(); - } - - public bool Contains(T t) - { - return set.Contains(t); - } - - public int IndexOf(T t) - { - return list.IndexOf(t); - } - - public IEnumerator GetEnumerator() - { - return list.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return list.GetEnumerator(); - } - } - - public class IdentityComparer : IEqualityComparer - { - public static IdentityComparer Instance = new(); - - public bool Equals(T x, T y) - { - return object.Equals(x, y); - } - - public int GetHashCode(T obj) - { - return RuntimeHelpers.GetHashCode(obj); - } - } - - public sealed class Utf8StringWriter : StringWriter - { - public override Encoding Encoding => Encoding.UTF8; - } - - public class FixedSizeQueue : IEnumerable - { - private Queue q = new Queue(); - - public int Limit { get; set; } - - public void Enqueue(T obj) - { - q.Enqueue(obj); - while (q.Count > Limit) q.Dequeue(); - } - - public IEnumerator GetEnumerator() - { - return q.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return q.GetEnumerator(); - } - } -} + *iflags |= (ushort)MethodImplOptions.NoInlining; + } + + public static object NewObjectNoCtor(Type type) + { + return FormatterServices.GetUninitializedObject(type); + } + + public static T NewObjectNoCtor() + { + return (T)NewObjectNoCtor(typeof(T)); + } + + private static bool ipv6; + private static bool ipv6Checked; + + // Socket.OSSupportsIPv6 doesn't seem to work + public static bool SupportsIPv6() + { + if (!ipv6Checked) + { + try + { + new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + ipv6 = true; + } + catch + { + ipv6 = false; + } + + ipv6Checked = true; + } + + return ipv6; + } + + public static string RwDataFile(string filename) + { + return Path.Combine(GenFilePaths.SaveDataFolderPath, filename); + } + + public static To ShallowCopy(From from, To to) where To : From + { + foreach (var f in AccessTools.GetDeclaredFields(typeof(From))) + if (!f.IsStatic) + f.SetValue(to, f.GetValue(from)); + + return to; + } + + public static string TranslateWithDoubleNewLines(string keyBase, int count) + { + return Enumerable.Range(1, count).Select(n => (keyBase + n).Translate()).Join(delimiter: "\n\n"); + } + + public static string DelegateMethodInfo(MethodBase m) + { + return + m == null + ? "No method" + : $"{m.DeclaringType.DeclaringType?.FullDescription()} {m.DeclaringType.FullDescription()} {m.Name}" + .Replace("<", "[").Replace(">", "]"); + } + + public static MethodBase GetOriginalFromHarmonyReplacement(long replacementAddr) + { + // Todo: this is using a non-public API of Harmony, we should refactor to use https://harmony.pardeike.net/api/HarmonyLib.Harmony.html#HarmonyLib_Harmony_GetOriginalMethod_System_Reflection_MethodInfo_ + // as pardeike suggested in https://github.com/rwmt/Multiplayer/pull/270#issuecomment-1003298289 + return HarmonySharedState.originals + .FirstOrDefault(kv => kv.Key.MethodHandle.GetFunctionPointer().ToInt64() == replacementAddr).Value; + } + + public static string JoinStringsAtMost(this IEnumerable strs, int atMost = 3) + { + return strs.Take(atMost).Join() + (strs.Count() > atMost ? "..." : ""); + } + } + + public struct Container + { + public T Inner { get; } + + public Container(T value) + { + Inner = value; + } + + public static implicit operator Container(T value) + { + return new Container(value); + } + } + + public class UniqueList : IEnumerable + { + private List list = new List(); + private HashSet set = new HashSet(); + + public int Count => list.Count; + public T this[int index] => list[index]; + + public bool Add(T t) + { + if (set.Add(t)) + { + list.Add(t); + return true; + } + + return false; + } + + public T[] ToArray() + { + return list.ToArray(); + } + + public bool Contains(T t) + { + return set.Contains(t); + } + + public int IndexOf(T t) + { + return list.IndexOf(t); + } + + public IEnumerator GetEnumerator() + { + return list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return list.GetEnumerator(); + } + } + + public class IdentityComparer : IEqualityComparer + { + public static IdentityComparer Instance = new(); + + public bool Equals(T x, T y) + { + return object.Equals(x, y); + } + + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } + + public sealed class Utf8StringWriter : StringWriter + { + public override Encoding Encoding => Encoding.UTF8; + } + + public class FixedSizeQueue : IEnumerable + { + private Queue q = new Queue(); + + public int Limit { get; set; } + + public void Enqueue(T obj) + { + q.Enqueue(obj); + while (q.Count > Limit) q.Dequeue(); + } + + public IEnumerator GetEnumerator() + { + return q.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return q.GetEnumerator(); + } + } +} diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 2f20117b2..e90e1c879 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -1,43 +1,43 @@ - - - + + + net48 - net10.0 - true - enable - 12 - false - false - Multiplayer.Common - MultiplayerCommon - - - - - - - - - - - - - - - - - - - - - - - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - - - - + net10.0 + true + enable + 12 + false + false + Multiplayer.Common + MultiplayerCommon + + + + + + + + + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + + + + diff --git a/Source/Common/Native.cs b/Source/Common/Native.cs index 8b64b7b52..e9c993bc1 100644 --- a/Source/Common/Native.cs +++ b/Source/Common/Native.cs @@ -1,275 +1,275 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using HarmonyLib; - -namespace Multiplayer.Client -{ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using HarmonyLib; + +namespace Multiplayer.Client +{ public static class Native { // Waylith uses CoreCLR. Never load Mono libraries or dereference Mono // thread structures on that runtime, even when running old mod assemblies. public static bool IsMonoRuntime => Type.GetType("Mono.Runtime") != null; - public enum NativeOS - { - Windows, OSX, Linux, Dummy - } - - public static IntPtr DomainPtr { get; private set; } - - // LMF is Last Managed Frame - // current_thread->internal_thread->thread_info->tls[4] - public static long LmfPtr { get; private set; } - - public static Func? HarmonyOriginalGetter { get; set; } - + public enum NativeOS + { + Windows, OSX, Linux, Dummy + } + + public static IntPtr DomainPtr { get; private set; } + + // LMF is Last Managed Frame + // current_thread->internal_thread->thread_info->tls[4] + public static long LmfPtr { get; private set; } + + public static Func? HarmonyOriginalGetter { get; set; } + public static void EarlyInit(NativeOS os) { if (!IsMonoRuntime) return; - if (os == NativeOS.Linux) - TheLinuxWay(); - if (os == NativeOS.OSX) - TheOSXWay(); - - EarlyInitInternal(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void TheLinuxWay() => mono_dllmap_insert_linux(IntPtr.Zero, MonoWindows, null, MonoLinux, null); - - [MethodImpl(MethodImplOptions.NoInlining)] - static void TheOSXWay() => mono_dllmap_insert_osx(IntPtr.Zero, MonoWindows, null, MonoOSX, null); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void EarlyInitInternal() - { - DomainPtr = mono_domain_get(); - } - + if (os == NativeOS.Linux) + TheLinuxWay(); + if (os == NativeOS.OSX) + TheOSXWay(); + + EarlyInitInternal(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TheLinuxWay() => mono_dllmap_insert_linux(IntPtr.Zero, MonoWindows, null, MonoLinux, null); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TheOSXWay() => mono_dllmap_insert_osx(IntPtr.Zero, MonoWindows, null, MonoOSX, null); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void EarlyInitInternal() + { + DomainPtr = mono_domain_get(); + } + public static unsafe void InitLmfPtr(NativeOS os) { if (!IsMonoRuntime) return; - // Don't bother on 32 bit runtimes - if (IntPtr.Size == 4) - return; - - const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | - BindingFlags.NonPublic; - - var internalThreadField = typeof(Thread).GetField("internal_thread", all); - var threadInfoField = internalThreadField.FieldType.GetField("runtime_thread_info", all); - var threadInfoPtr = (long)(IntPtr)threadInfoField.GetValue(internalThreadField.GetValue(Thread.CurrentThread)); - - // Struct offset found manually - // Navigate by string: "Handle Stack" - // Updated for RimWorld 1.6 (Unity 2022.3.35f1, Mono 6.13.0) - if (os == NativeOS.Linux) - LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) - else if (os == NativeOS.Windows) - LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. - else if (os == NativeOS.OSX) - LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) - else if (os == NativeOS.Dummy) - { - LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); - *(long*)LmfPtr = LmfPtr; - *(long*)(LmfPtr + 8) = 0; - } - } - - private static IntPtr MaybeFindHarmonyOriginalMethod(long addr, bool harmonyOriginals) - { - var ji = mono_jit_info_table_find(DomainPtr, (IntPtr)addr); - if (ji == IntPtr.Zero) return IntPtr.Zero; - - var ptr = mono_jit_info_get_method(ji); - var codeStart = (long)mono_jit_info_get_code_start(ji); - - if (harmonyOriginals && HarmonyOriginalGetter != null) - { - var original = HarmonyOriginalGetter(codeStart); - if (original != null) - ptr = original.MethodHandle.Value; - } - - return ptr; - } - - public static string? MethodNameFromAddr(long addr, bool harmonyOriginals) - { - var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); - if (ptr == IntPtr.Zero) return null; - var name = mono_debug_print_stack_frame(ptr, -1, DomainPtr); - return string.IsNullOrEmpty(name) ? null : name; - } - - public static string? MethodNameNormalizedFromAddr(long addr, bool harmonyOriginals) - { - var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); - return ptr == IntPtr.Zero ? null : mono_method_get_reflection_name(ptr); - } - - private static ConstructorInfo runtimeMethodHandleCtor = AccessTools.Constructor(typeof(RuntimeMethodHandle), [typeof(IntPtr)]); - - public static bool GetMethodAggressiveInlining(long addr) - { - var domain = DomainPtr; - var ji = mono_jit_info_table_find(domain, (IntPtr)addr); - - if (ji == IntPtr.Zero) return false; - - var methodHandle = mono_jit_info_get_method(ji); - var methodBase = GetMethodBaseFromRuntimePointer(methodHandle); - - if (methodBase == null) return false; - - return (methodBase.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0; - } - - public static MethodBase? GetMethodBaseFromRuntimePointer(IntPtr methodHandle) - { - if (methodHandle == IntPtr.Zero) - return null; - var rmh = (RuntimeMethodHandle)runtimeMethodHandleCtor.Invoke([methodHandle]); - var rth = new RuntimeTypeHandle(); - var methodBase = MethodBase.GetMethodFromHandle(rmh, rth); - return methodBase; - } - - // const string MonoWindows = "mono-2.0-sgen.dll"; - const string MonoWindows = "mono-2.0-bdwgc"; - const string MonoLinux = "libmonobdwgc-2.0.so"; - const string MonoOSX = "libmonobdwgc-2.0.dylib"; - - [DllImport(MonoLinux, EntryPoint = "mono_dllmap_insert")] - private static extern void mono_dllmap_insert_linux(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); - - [DllImport(MonoOSX, EntryPoint = "mono_dllmap_insert")] - private static extern void mono_dllmap_insert_osx(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_table_find(IntPtr domain, IntPtr addr); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_get_method(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_jit_info_get_code_start(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern int mono_jit_info_get_code_size(IntPtr ji); - - [DllImport(MonoWindows)] - public static extern string mono_debug_print_stack_frame(IntPtr method, int nativeOffset, IntPtr domain); - - [DllImport(MonoWindows)] - public static extern int mini_parse_debug_option(string option); - - [DllImport(MonoWindows)] - public static extern void mono_set_defaults(int verboseLevel, int opts); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_domain_get(); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_compile_method(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_from_mono_type(IntPtr type); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_vtable(IntPtr domain, IntPtr klass); - - [DllImport(MonoWindows)] - public static extern int mono_method_get_token(IntPtr method); - - [DllImport(MonoWindows)] - public static extern string mono_method_get_reflection_name(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_method_get_class(IntPtr method); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_class_get_image(IntPtr klass); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_profiler_create(IntPtr profiler); - - public delegate void JitDoneCallback(IntPtr profiler, IntPtr method, IntPtr jinfo); - - [DllImport(MonoWindows)] - public static extern IntPtr mono_profiler_set_jit_done_callback(IntPtr profiler, JitDoneCallback? cb); - - public static unsafe bool CctorRan(Type t) - { - return *((byte*)mono_class_vtable(mono_domain_get(), mono_class_from_mono_type(t.TypeHandle.Value)) + 45) != 0; - } - } - - /*public static class IcedDisasm - { - public unsafe static void DisasmMethod(long codeStart) - { - DisasmMethod(codeStart, Native.mono_jit_info_get_code_size(Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)codeStart))); - } - - public unsafe static void DisasmMethod(long codeStart, long codeSize) - { - //Log.Message($"code {Convert.ToString(*(uint*)codeStart, 16)}"); - //return; - - var codeRip = (ulong)codeStart; - var codeReader = new UnsafeCodeReader() { start = (byte*)codeStart }; - var decoder = Decoder.Create(64, codeReader); - decoder.IP = codeRip; - ulong endRip = decoder.IP + (uint)codeSize + 8; - - var instructions = new InstructionList(); - while (decoder.IP < endRip) - decoder.Decode(out instructions.AllocUninitializedElement()); - - var formatter = new NasmFormatter(); - formatter.Options.DigitSeparator = "`"; - formatter.Options.FirstOperandCharIndex = 10; - var output = new StringOutput(); - - bool cond = (*(uint*)codeStart & 0xFFFFFF) != 0xEC8348 && *(byte*)codeStart != 0x55; - - Log.Message("Code size: " + codeSize, true); - - foreach (ref var instr in instructions) - { - var stringb = new StringBuilder(); - formatter.Format(instr, output); - stringb.Append(instr.IP.ToString("X16")); - stringb.Append(" "); - int instrLen = instr.Length; - int byteBaseIndex = (int)(instr.IP - codeRip); - for (int i = 0; i < instrLen; i++) - stringb.Append(((byte*)codeStart)[byteBaseIndex + i].ToString("X2")); - int missingBytes = 10 - instrLen; - for (int i = 0; i < missingBytes; i++) - stringb.Append(" "); - stringb.Append(" "); - stringb.Append(output.ToStringAndReset()); - //if (cond) - Log.Message(stringb.ToString(), ignoreStopLoggingLimit: true); - } - } - - public unsafe class UnsafeCodeReader : CodeReader - { - public byte* start; - - public unsafe override int ReadByte() - { - var val = *start; - start++; - return val; - } - } - }*/ -} + // Don't bother on 32 bit runtimes + if (IntPtr.Size == 4) + return; + + const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic; + + var internalThreadField = typeof(Thread).GetField("internal_thread", all); + var threadInfoField = internalThreadField.FieldType.GetField("runtime_thread_info", all); + var threadInfoPtr = (long)(IntPtr)threadInfoField.GetValue(internalThreadField.GetValue(Thread.CurrentThread)); + + // Struct offset found manually + // Navigate by string: "Handle Stack" + // Updated for RimWorld 1.6 (Unity 2022.3.35f1, Mono 6.13.0) + if (os == NativeOS.Linux) + LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) + else if (os == NativeOS.Windows) + LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. + else if (os == NativeOS.OSX) + LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) + else if (os == NativeOS.Dummy) + { + LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); + *(long*)LmfPtr = LmfPtr; + *(long*)(LmfPtr + 8) = 0; + } + } + + private static IntPtr MaybeFindHarmonyOriginalMethod(long addr, bool harmonyOriginals) + { + var ji = mono_jit_info_table_find(DomainPtr, (IntPtr)addr); + if (ji == IntPtr.Zero) return IntPtr.Zero; + + var ptr = mono_jit_info_get_method(ji); + var codeStart = (long)mono_jit_info_get_code_start(ji); + + if (harmonyOriginals && HarmonyOriginalGetter != null) + { + var original = HarmonyOriginalGetter(codeStart); + if (original != null) + ptr = original.MethodHandle.Value; + } + + return ptr; + } + + public static string? MethodNameFromAddr(long addr, bool harmonyOriginals) + { + var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); + if (ptr == IntPtr.Zero) return null; + var name = mono_debug_print_stack_frame(ptr, -1, DomainPtr); + return string.IsNullOrEmpty(name) ? null : name; + } + + public static string? MethodNameNormalizedFromAddr(long addr, bool harmonyOriginals) + { + var ptr = MaybeFindHarmonyOriginalMethod(addr, harmonyOriginals); + return ptr == IntPtr.Zero ? null : mono_method_get_reflection_name(ptr); + } + + private static ConstructorInfo runtimeMethodHandleCtor = AccessTools.Constructor(typeof(RuntimeMethodHandle), [typeof(IntPtr)]); + + public static bool GetMethodAggressiveInlining(long addr) + { + var domain = DomainPtr; + var ji = mono_jit_info_table_find(domain, (IntPtr)addr); + + if (ji == IntPtr.Zero) return false; + + var methodHandle = mono_jit_info_get_method(ji); + var methodBase = GetMethodBaseFromRuntimePointer(methodHandle); + + if (methodBase == null) return false; + + return (methodBase.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0; + } + + public static MethodBase? GetMethodBaseFromRuntimePointer(IntPtr methodHandle) + { + if (methodHandle == IntPtr.Zero) + return null; + var rmh = (RuntimeMethodHandle)runtimeMethodHandleCtor.Invoke([methodHandle]); + var rth = new RuntimeTypeHandle(); + var methodBase = MethodBase.GetMethodFromHandle(rmh, rth); + return methodBase; + } + + // const string MonoWindows = "mono-2.0-sgen.dll"; + const string MonoWindows = "mono-2.0-bdwgc"; + const string MonoLinux = "libmonobdwgc-2.0.so"; + const string MonoOSX = "libmonobdwgc-2.0.dylib"; + + [DllImport(MonoLinux, EntryPoint = "mono_dllmap_insert")] + private static extern void mono_dllmap_insert_linux(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); + + [DllImport(MonoOSX, EntryPoint = "mono_dllmap_insert")] + private static extern void mono_dllmap_insert_osx(IntPtr assembly, string? dll, string? func, string? tdll, string? tfunc); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_table_find(IntPtr domain, IntPtr addr); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_get_method(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_jit_info_get_code_start(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern int mono_jit_info_get_code_size(IntPtr ji); + + [DllImport(MonoWindows)] + public static extern string mono_debug_print_stack_frame(IntPtr method, int nativeOffset, IntPtr domain); + + [DllImport(MonoWindows)] + public static extern int mini_parse_debug_option(string option); + + [DllImport(MonoWindows)] + public static extern void mono_set_defaults(int verboseLevel, int opts); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_domain_get(); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_compile_method(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_from_mono_type(IntPtr type); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_vtable(IntPtr domain, IntPtr klass); + + [DllImport(MonoWindows)] + public static extern int mono_method_get_token(IntPtr method); + + [DllImport(MonoWindows)] + public static extern string mono_method_get_reflection_name(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_method_get_class(IntPtr method); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_class_get_image(IntPtr klass); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_profiler_create(IntPtr profiler); + + public delegate void JitDoneCallback(IntPtr profiler, IntPtr method, IntPtr jinfo); + + [DllImport(MonoWindows)] + public static extern IntPtr mono_profiler_set_jit_done_callback(IntPtr profiler, JitDoneCallback? cb); + + public static unsafe bool CctorRan(Type t) + { + return *((byte*)mono_class_vtable(mono_domain_get(), mono_class_from_mono_type(t.TypeHandle.Value)) + 45) != 0; + } + } + + /*public static class IcedDisasm + { + public unsafe static void DisasmMethod(long codeStart) + { + DisasmMethod(codeStart, Native.mono_jit_info_get_code_size(Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)codeStart))); + } + + public unsafe static void DisasmMethod(long codeStart, long codeSize) + { + //Log.Message($"code {Convert.ToString(*(uint*)codeStart, 16)}"); + //return; + + var codeRip = (ulong)codeStart; + var codeReader = new UnsafeCodeReader() { start = (byte*)codeStart }; + var decoder = Decoder.Create(64, codeReader); + decoder.IP = codeRip; + ulong endRip = decoder.IP + (uint)codeSize + 8; + + var instructions = new InstructionList(); + while (decoder.IP < endRip) + decoder.Decode(out instructions.AllocUninitializedElement()); + + var formatter = new NasmFormatter(); + formatter.Options.DigitSeparator = "`"; + formatter.Options.FirstOperandCharIndex = 10; + var output = new StringOutput(); + + bool cond = (*(uint*)codeStart & 0xFFFFFF) != 0xEC8348 && *(byte*)codeStart != 0x55; + + Log.Message("Code size: " + codeSize, true); + + foreach (ref var instr in instructions) + { + var stringb = new StringBuilder(); + formatter.Format(instr, output); + stringb.Append(instr.IP.ToString("X16")); + stringb.Append(" "); + int instrLen = instr.Length; + int byteBaseIndex = (int)(instr.IP - codeRip); + for (int i = 0; i < instrLen; i++) + stringb.Append(((byte*)codeStart)[byteBaseIndex + i].ToString("X2")); + int missingBytes = 10 - instrLen; + for (int i = 0; i < missingBytes; i++) + stringb.Append(" "); + stringb.Append(" "); + stringb.Append(output.ToStringAndReset()); + //if (cond) + Log.Message(stringb.ToString(), ignoreStopLoggingLimit: true); + } + } + + public unsafe class UnsafeCodeReader : CodeReader + { + public byte* start; + + public unsafe override int ReadByte() + { + var val = *start; + start++; + return val; + } + } + }*/ +} diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index cd008eaea..e2eb6b50d 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -1,38 +1,38 @@ - - - + + + net48 - net10.0 - true - 12 - false - false - MultiplayerLoader - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ - - + net10.0 + true + 12 + false + false + MultiplayerLoader + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(AssemblyName)/ + + diff --git a/Source/MultiplayerLoader/Prepatches.cs b/Source/MultiplayerLoader/Prepatches.cs index a42334c42..348d69b89 100644 --- a/Source/MultiplayerLoader/Prepatches.cs +++ b/Source/MultiplayerLoader/Prepatches.cs @@ -1,85 +1,85 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using MonoMod.Utils; -using Multiplayer.Common; -using Prepatcher; -using RimWorld; - -namespace MultiplayerLoader; - -public static class Prepatches -{ - [FreePatch] - static void DontSetResolution(ModuleDefinition module) - { - if (MpVersion.IsDebug) - { - var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); - resolutionUtilityType.FindMethod(nameof(ResolutionUtility.SetResolutionRaw))!.Body.Instructions.Insert( - 0, - Instruction.Create(OpCodes.Ret) - ); - } - } - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; +using Multiplayer.Common; +using Prepatcher; +using RimWorld; + +namespace MultiplayerLoader; + +public static class Prepatches +{ + [FreePatch] + static void DontSetResolution(ModuleDefinition module) + { + if (MpVersion.IsDebug) + { + var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); + resolutionUtilityType.FindMethod(nameof(ResolutionUtility.SetResolutionRaw))!.Body.Instructions.Insert( + 0, + Instruction.Create(OpCodes.Ret) + ); + } + } + [FreePatch] static void ApplySyncPrepatches(ModuleDefinition module) { #if !RIMWORLD_1_7 SyncRituals.ApplyPrepatches(module); #endif - } - - internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix) - { - var targetMethod = module.ImportReference(target).Resolve(); - var firstInst = targetMethod.Body.Instructions.First(); - - IEnumerable PrefixInstructions() - { - yield return Instruction.Create(OpCodes.Ldc_I4, id); // Load index - yield return Instruction.Create(OpCodes.Ldarg_0); // Load __instance - - // Create __args array - yield return Instruction.Create(OpCodes.Ldc_I4, target.GetParameters().Length); - yield return Instruction.Create(OpCodes.Newarr, module.TypeSystem.Object); - - int idx = 0; - foreach (var pInfo in target.GetParameters()) - { - var pType = pInfo.ParameterType; - - yield return Instruction.Create(OpCodes.Dup); - yield return Instruction.Create(OpCodes.Ldc_I4, idx); - yield return Instruction.Create(OpCodes.Ldarg, targetMethod.Parameters.ElementAt(idx)); - - if (pType.IsByRef) - throw new Exception("Byrefs aren't handled"); - if (pType.IsValueType) - yield return Instruction.Create(OpCodes.Box, module.ImportReference(pType)); - - yield return Instruction.Create(OpCodes.Stelem_Ref); - - idx++; - } - - yield return Instruction.Create(OpCodes.Call, module.ImportReference(prefix)); - yield return Instruction.Create(OpCodes.Brtrue, firstInst); - - if (target.ReturnType == typeof(bool)) - yield return Instruction.Create(OpCodes.Ldc_I4_0); - else if (target.ReturnType == typeof(void)) - yield return Instruction.Create(OpCodes.Nop); - else - throw new Exception($"Can't handle return type {target.ReturnType}"); - - yield return Instruction.Create(OpCodes.Ret); - } - - targetMethod.Body.Instructions.InsertRange(0, PrefixInstructions()); - } -} + } + + internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix) + { + var targetMethod = module.ImportReference(target).Resolve(); + var firstInst = targetMethod.Body.Instructions.First(); + + IEnumerable PrefixInstructions() + { + yield return Instruction.Create(OpCodes.Ldc_I4, id); // Load index + yield return Instruction.Create(OpCodes.Ldarg_0); // Load __instance + + // Create __args array + yield return Instruction.Create(OpCodes.Ldc_I4, target.GetParameters().Length); + yield return Instruction.Create(OpCodes.Newarr, module.TypeSystem.Object); + + int idx = 0; + foreach (var pInfo in target.GetParameters()) + { + var pType = pInfo.ParameterType; + + yield return Instruction.Create(OpCodes.Dup); + yield return Instruction.Create(OpCodes.Ldc_I4, idx); + yield return Instruction.Create(OpCodes.Ldarg, targetMethod.Parameters.ElementAt(idx)); + + if (pType.IsByRef) + throw new Exception("Byrefs aren't handled"); + if (pType.IsValueType) + yield return Instruction.Create(OpCodes.Box, module.ImportReference(pType)); + + yield return Instruction.Create(OpCodes.Stelem_Ref); + + idx++; + } + + yield return Instruction.Create(OpCodes.Call, module.ImportReference(prefix)); + yield return Instruction.Create(OpCodes.Brtrue, firstInst); + + if (target.ReturnType == typeof(bool)) + yield return Instruction.Create(OpCodes.Ldc_I4_0); + else if (target.ReturnType == typeof(void)) + yield return Instruction.Create(OpCodes.Nop); + else + throw new Exception($"Can't handle return type {target.ReturnType}"); + + yield return Instruction.Create(OpCodes.Ret); + } + + targetMethod.Body.Instructions.InsertRange(0, PrefixInstructions()); + } +} diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs index cb845dab1..02aa5aa2a 100644 --- a/Source/MultiplayerLoader/SyncRituals.cs +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -1,17 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using Mono.Cecil; -using Multiplayer.API; -using Multiplayer.Client.Util; -using RimWorld; - -namespace MultiplayerLoader; - -public static class SyncRituals -{ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Mono.Cecil; +using Multiplayer.API; +using Multiplayer.Client.Util; +using RimWorld; + +namespace MultiplayerLoader; + +public static class SyncRituals +{ private static List syncMethodsWithPrepatches = new(); #if RIMWORLD_1_7 private static readonly Dictionary<(Type, string), ISyncMethodForPrepatcher> runtimeSyncMethods = new(); @@ -24,41 +24,41 @@ public static bool RuntimePrefix(object __instance, object[] __args, MethodBase || !sync.DoSync(__instance, __args); } #endif - - public static bool PrepatcherPrefix(int index, object __instance, object[] __args) - { - // This prefix is applied to a generic method and called by all possible instantiations - // This check makes sure the type is correct - if (syncMethodsWithPrepatches[index].TargetType == __instance.GetType()) - return !syncMethodsWithPrepatches[index].DoSync(__instance, __args); - return true; - } - - private static MethodInfo syncMethod = AccessTools.Method("Multiplayer.Client.Sync:Method"); - - // This method is called twice (two passes) - // The first pass with module != null is called from Prepatcher and applies prepatches - // The second pass with module == null is called from Sync initialization and registers SyncMethods - public static void ApplyPrepatches(ModuleDefinition module) - { - void Register(Type baseType, string method, Type derivedType, Action postProcess = null) - { - if (module != null) - { - Prepatches.AddPrepatcherPrefix( - module, - syncMethodsWithPrepatches.Count, - AccessTools.Method(baseType, method), - AccessTools.Method(typeof(SyncRituals), nameof(PrepatcherPrefix)) - ); - - // The ids need to be in sync between passes (note the array is recreated after Prepatcher restarts the game) - syncMethodsWithPrepatches.Add(null); - } - else - { - // Sync.Method doesn't apply a Harmony patch and doesn't require the DeclaringType to be the target type - var newSyncMethod = (ISyncMethodForPrepatcher)syncMethod.Invoke(null, [derivedType, method, null]); + + public static bool PrepatcherPrefix(int index, object __instance, object[] __args) + { + // This prefix is applied to a generic method and called by all possible instantiations + // This check makes sure the type is correct + if (syncMethodsWithPrepatches[index].TargetType == __instance.GetType()) + return !syncMethodsWithPrepatches[index].DoSync(__instance, __args); + return true; + } + + private static MethodInfo syncMethod = AccessTools.Method("Multiplayer.Client.Sync:Method"); + + // This method is called twice (two passes) + // The first pass with module != null is called from Prepatcher and applies prepatches + // The second pass with module == null is called from Sync initialization and registers SyncMethods + public static void ApplyPrepatches(ModuleDefinition module) + { + void Register(Type baseType, string method, Type derivedType, Action postProcess = null) + { + if (module != null) + { + Prepatches.AddPrepatcherPrefix( + module, + syncMethodsWithPrepatches.Count, + AccessTools.Method(baseType, method), + AccessTools.Method(typeof(SyncRituals), nameof(PrepatcherPrefix)) + ); + + // The ids need to be in sync between passes (note the array is recreated after Prepatcher restarts the game) + syncMethodsWithPrepatches.Add(null); + } + else + { + // Sync.Method doesn't apply a Harmony patch and doesn't require the DeclaringType to be the target type + var newSyncMethod = (ISyncMethodForPrepatcher)syncMethod.Invoke(null, [derivedType, method, null]); postProcess?.Invoke(newSyncMethod); syncMethodsWithPrepatches.Add(newSyncMethod); #if RIMWORLD_1_7 @@ -68,86 +68,86 @@ void Register(Type baseType, string method, Type derivedType, Action, (PawnRitualRoleSelectionWidget roleSelectionWidget, string Key)>( - RitualRoleWriter, - RitualRoleReader - ); - var psychicRitualRolesSerializer = Serializer.New, (PawnPsychicRitualRoleSelectionWidget roleSelectionWidget, string Key)>( - RitualRoleWriter, - RitualRoleReader - ); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), - typeof(PawnRitualRoleSelectionWidget), - m => m.TransformArgument(1, ritualRolesSerializer)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), - typeof(PawnPsychicRitualRoleSelectionWidget), - m => m.TransformArgument(1, psychicRitualRolesSerializer)); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), - typeof(PawnRitualRoleSelectionWidget)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), - typeof(PawnPsychicRitualRoleSelectionWidget)); - - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), - typeof(PawnRitualRoleSelectionWidget), - m => m.TransformArgument(1, ritualRolesSerializer)); - Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), - typeof(PawnPsychicRitualRoleSelectionWidget), - m => m.TransformArgument(1, psychicRitualRolesSerializer)); - - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 7).Name, - typeof(PawnRitualRoleSelectionWidget) - ); // Roles right click delegate (try assign spectate) - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 7).Name, - typeof(PawnPsychicRitualRoleSelectionWidget) - ); // Roles right click delegate (try assign spectate) - - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 2).Name, - typeof(PawnRitualRoleSelectionWidget) - ); // Not participating left click delegate (try assign any role or spectate) - Register( - typeof(PawnRoleSelectionWidgetBase<>), - MpMethodUtil.GetLambda( - typeof(PawnRoleSelectionWidgetBase), - nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), - lambdaOrdinal: 2).Name, - typeof(PawnPsychicRitualRoleSelectionWidget) - ); // Not participating left click delegate (try assign any role or spectate) - } - - private static (WidgetType roleSelectionWidget, string Key) RitualRoleWriter(IEnumerable roles, object target, object[] _) - where WidgetType : PawnRoleSelectionWidgetBase - where RoleType : class, ILordJobRole - { - var roleSelectionWidget = (WidgetType)target; - return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); - } - - private static IEnumerable RitualRoleReader((WidgetType roleSelectionWidget, string Key) data) - where WidgetType : PawnRoleSelectionWidgetBase - where RoleType : class, ILordJobRole - { - return data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key); - } -} + } + } + + // ======= PawnRitualRoleSelectionWidget ======= + + var ritualRolesSerializer = Serializer.New, (PawnRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader + ); + var psychicRitualRolesSerializer = Serializer.New, (PawnPsychicRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader + ); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnRitualRoleSelectionWidget)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnPsychicRitualRoleSelectionWidget)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + } + + private static (WidgetType roleSelectionWidget, string Key) RitualRoleWriter(IEnumerable roles, object target, object[] _) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + var roleSelectionWidget = (WidgetType)target; + return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); + } + + private static IEnumerable RitualRoleReader((WidgetType roleSelectionWidget, string Key) data) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + return data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key); + } +} diff --git a/Source/Tests/Tests.csproj b/Source/Tests/Tests.csproj index ecde80b82..1264869cb 100644 --- a/Source/Tests/Tests.csproj +++ b/Source/Tests/Tests.csproj @@ -1,33 +1,33 @@ - - - + + + net8.0 - net10.0 - enable - enable - 12 - - false - - - - - - - - - - - - - - - - - - - - - - - + net10.0 + enable + enable + 12 + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt b/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt index 08c775816..c5901528d 100644 --- a/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt +++ b/Source/Tests/packet-serializations/ClientInitDataPacket.verified.txt @@ -1,3 +1,3 @@ -This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object +This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object 05-00-00-00-31-2E-30-2E-30-04-00-00-00-01-00-00-00-02-00-00-00-03-00-00-00-04-00-00-00-01-00-00-00-01-00-00-00-00-00-00-03-02-00-00-00-03-00-00-00-6B-65-79-01-00-00-00-7B-00-00-00-04-00-00-00-6B-65-79-32-00-00-00-00-00-00-00-00-00-26-00-00-00-63-64-60-60-10-60-60-60-28-2A-CF-2D-D1-CB-2D-CD-29-C9-2C-C8-49-AC-4C-2D-E2-66-60-60-F0-45-70-19-A0-80-19-4C-01-00 (119 bytes) diff --git a/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt b/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt index 0e11269af..17e380770 100644 --- a/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt +++ b/Source/Tests/packet-serializations/ServerJoinDataPacket.verified.txt @@ -1,3 +1,3 @@ -This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object +This packet is not byte-stable while serialized, meaning it can be serialized differently due to various factors, but it does deserialize into the same object 08-00-00-00-47-61-6D-65-4E-61-6D-65-01-00-00-00-08-00-00-00-31-2E-36-2E-34-35-36-36-0D-00-00-00-30-2E-31-31-2E-30-2B-31-32-33-34-35-36-05-00-00-00-00-00-02-03-01-00-26-00-00-00-63-64-60-60-10-60-60-60-28-2A-CF-2D-D1-CB-2D-CD-29-C9-2C-C8-49-AC-4C-2D-E2-66-60-60-F0-45-70-19-A0-80-19-4C-01-00 (97 bytes) From fb025c5edce6396564ddc108e0b0d857fa1cc7c5 Mon Sep 17 00:00:00 2001 From: mkiplm-svg Date: Tue, 20 Oct 2026 00:36:16 +0000 Subject: [PATCH 3/3] Avoid CoreCLR stack trace false desyncs --- Source/Client/Desyncs/ClientSyncOpinion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Client/Desyncs/ClientSyncOpinion.cs b/Source/Client/Desyncs/ClientSyncOpinion.cs index 0ab3482f3..cbc052ccf 100644 --- a/Source/Client/Desyncs/ClientSyncOpinion.cs +++ b/Source/Client/Desyncs/ClientSyncOpinion.cs @@ -53,7 +53,7 @@ join map2 in other.mapStates on map1.mapId equals map2.mapId if (!commandRandomStates.SequenceEqual(other.commandRandomStates)) return "Random state from commands doesn't match"; - if (!simulating && !other.simulating && desyncStackTraceHashes.Any() && other.desyncStackTraceHashes.Any() && !desyncStackTraceHashes.SequenceEqual(other.desyncStackTraceHashes)) + if (Native.IsMonoRuntime && !simulating && !other.simulating && desyncStackTraceHashes.Any() && other.desyncStackTraceHashes.Any() && !desyncStackTraceHashes.SequenceEqual(other.desyncStackTraceHashes)) return "Trace hashes don't match"; return null;