diff --git a/Source/Mods/UiNotIncluded.cs b/Source/Mods/UiNotIncluded.cs new file mode 100644 index 0000000..98f601c --- /dev/null +++ b/Source/Mods/UiNotIncluded.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections; +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Compat; + +/// UI Not Included: Customizable UI Overhaul by GonDragon +/// +/// +[MpCompatFor("GonDragon.UINotIncluded")] +public class UiNotIncluded +{ + #region MainPatch + + public UiNotIncluded(ModContentPack mod) => LongEventHandler.ExecuteWhenFinished(LatePatch); + + private static void LatePatch() + { + string errorType; + void LogError(string error) => Log.Error($"Patching UI Not Included failed ({errorType}): {error}"); + + #region Hotkey + + { + errorType = "time control hotkeys"; + + var type = AccessTools.TypeByName("Multiplayer.Client.AsyncTime.TimeControlPatch"); + var doTimeControlsHotkeys = AccessTools.DeclaredMethod(type, "DoTimeControlsHotkeys"); + var timespeedConfig = AccessTools.TypeByName("UINotIncluded.Widget.Configs.TimespeedConfig"); + type = AccessTools.TypeByName("UINotIncluded.Settings"); + var vanillaControlSpeed = AccessTools.DeclaredField(type, "vanillaControlSpeed"); + var topBar = AccessTools.DeclaredField(type, "topBar"); + var bottomBar = AccessTools.DeclaredField(type, "bottomBar"); + var settingsExposeDataMethod = AccessTools.DeclaredMethod(type, "ExposeData"); + type = AccessTools.TypeByName("UINotIncluded.UINI_Mod"); + var doSettingsWindowMethod = AccessTools.DeclaredMethod(type, "DoSettingsWindowContents"); + + // Since failing to patch the mod will most likely result in a game that can't be + // unpaused and/or constant errors, handle patching with way too much extra safety. + if (doTimeControlsHotkeys == null) + LogError("MP's DoTimeControlsHotkeys method doesn't exist."); + else if (!doTimeControlsHotkeys.IsStatic) + LogError("MP's DoTimeControlsHotkeys method is not static."); + else if (doTimeControlsHotkeys.ReturnType != typeof(void)) + LogError("MP's DoTimeControlsHotkeys method does not have a void return type."); + else if (doTimeControlsHotkeys.GetParameters() is not { Length: 0 }) + LogError($"MP's DoTimeControlsHotkeys method has incorrect number of arguments."); + else if (timespeedConfig == null) + LogError("TimespeedConfig type is null."); + else if(vanillaControlSpeed == null) + LogError("vanillaControlSpeed field is null"); + else if (!vanillaControlSpeed.IsStatic) + LogError("vanillaControlSpeed field is not static."); + else if (vanillaControlSpeed.FieldType != typeof(bool)) + LogError("vanillaControlSpeed field is not of type bool."); + else if(topBar == null) + LogError("topBar field is null."); + else if (!topBar.IsStatic) + LogError("topBar field is not static."); + else if (!typeof(IList).IsAssignableFrom(topBar.FieldType)) + LogError("topBar field is not of type IList."); + else if(bottomBar == null) + LogError("bottomBar field is null."); + else if (!bottomBar.IsStatic) + LogError("bottomBar field is not static."); + else if (!typeof(IList).IsAssignableFrom(bottomBar.FieldType)) + LogError("bo$ttomBar field is not of type IList."); + else if (settingsExposeDataMethod == null) + LogError("ExposeData method is null."); + else if (doSettingsWindowMethod == null) + LogError("DoSettingsWindowContents method is null."); + else + { + doTimeControlsHotkeysMethod = MethodInvoker.GetHandler(doTimeControlsHotkeys); + timespeedConfigType = timespeedConfig; + vanillaControlSpeedField = AccessTools.StaticFieldRefAccess(vanillaControlSpeed); + topBarField = AccessTools.StaticFieldRefAccess(topBar); + bottomBarField = AccessTools.StaticFieldRefAccess(bottomBar); + + MpCompat.harmony.Patch(AccessTools.DeclaredMethod(typeof(GlobalControlsUtility), nameof(GlobalControlsUtility.DoTimespeedControls)), + new HarmonyMethod(PreDoTimespeedControls)); + MpCompat.harmony.Patch(doSettingsWindowMethod, new HarmonyMethod(PostPotentialSettingsChange)); + MpCompat.harmony.Patch(settingsExposeDataMethod, new HarmonyMethod(PostPotentialSettingsChange)); + } + } + + #endregion + + #region Time Controls Widget + + { + errorType = "time control widget"; + + var type = AccessTools.TypeByName("UINotIncluded.Widget.Workers.Timespeed_Worker"); + var timeControlsMethodField = AccessTools.DeclaredField(type, "cached_DoTimeControlsGUI"); + type = AccessTools.TypeByName("Multiplayer.Client.AsyncTime.TimeControlPatch"); + var mpDoGuiMethod = AccessTools.DeclaredMethod(type, "DoTimeControlsGUI"); + + // Since failing to patch the mod will most likely result in a game that can't be + // unpaused and/or constant errors, handle patching with way too much extra safety. + if (timeControlsMethodField == null) + LogError("cached time control field doesn't exist."); + else if (!timeControlsMethodField.IsStatic) + LogError("cached time control field is not static."); + else if (timeControlsMethodField.FieldType != typeof(Action)) + LogError("cached time control field type is not Action."); + else if (mpDoGuiMethod == null) + LogError("MP's DoTimeControlsGUI method doesn't exist."); + else if (!mpDoGuiMethod.IsStatic) + LogError("MP's DoTimeControlsGUI method is not static."); + else if (mpDoGuiMethod.ReturnType != typeof(void)) + LogError("MP's DoTimeControlsGUI method does not have a void return type."); + else if (mpDoGuiMethod.GetParameters() is not { Length: 1 } parms) + LogError("MP's DoTimeControlsGUI method has incorrect number of arguments."); + else if (parms[0].ParameterType != typeof(Rect)) + LogError("MP's DoTimeControlsGUI method argument is not of type Rect."); + // Replace the mod's cached delegate to vanilla DoTimeControlsGUI + // method with MP's DoTimeControlsGUI method instead. + else + timeControlsMethodField.SetValue(null, Delegate.CreateDelegate(typeof(Action), mpDoGuiMethod)); + } + + #endregion + + // Things unchanged: setting the "play settings at top" button will cause them to overlap + // with Multiplayer chat and (if they're enabled) debug buttons. Since it's an option + // I'm not going to bother changing this at all. + } + + #endregion + + #region Hotkey patches + + // MP Compat + private static bool needToListenToHotkeys = false; + // MP + private static FastInvokeHandler doTimeControlsHotkeysMethod; + // UI Not Included + private static Type timespeedConfigType; + private static AccessTools.FieldRef vanillaControlSpeedField; + private static AccessTools.FieldRef topBarField; + private static AccessTools.FieldRef bottomBarField; + + private static void PreDoTimespeedControls() + { + // The mod prevents vanilla code from running here, unless + // vanillaControlSpeed settings is enabled. Additionally, + // we need to consider the fact that a timespeed widget + // may be active somewhere in the mod already, in which + // case we don't need to do this call either. + if (needToListenToHotkeys) + doTimeControlsHotkeysMethod(null); + } + + private static void PostPotentialSettingsChange() + { + if (vanillaControlSpeedField()) + { + needToListenToHotkeys = false; + return; + } + + foreach (var bar in new[] { topBarField(), bottomBarField() }) + { + if (bar != null) + { + foreach (var obj in bar) + { + if (timespeedConfigType.IsInstanceOfType(obj)) + { + needToListenToHotkeys = false; + return; + } + } + } + } + + needToListenToHotkeys = true; + } + + #endregion +} \ No newline at end of file