From 44075420b06a442d01ed305a31d34bcc8d03572d Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 2 Sep 2024 14:23:33 +0200 Subject: [PATCH] Update Ancient urban ruins compat A recent update added a main tab for multi-floor map schedules to allow/disallow pawns from automatically going to specific floors at specific times of the day. There were also a couple of other features, but looks like this is the only one that needed syncing. Because MP right now can't handle syncing pocket map world objects, a new sync worker for it needed to be added. However, since `PocketMapParent` would be synced implicitly by `WorldObject` sync worker, we need to skip the `HasSyncWorker` check as it would return true and gives us a warning. I've added a `HashSet` that holds all types that were passed to this method, skipping execution if it already contained it. This ensures that sync workers skipping `HasSyncWorker` check will only be registered 1 at most, as well as preventing warnings that "sync worker exists in MP" if we attempt to register the same one 2 or more times. This will also ensure that warning due to MP having the sync worker and errors due to unsupported types will at most be displayed once per type. --- Source/Mods/AncientUrbanRuins.cs | 155 ++++++++++++++++++++++++++++++- Source/MpSyncWorkers.cs | 39 ++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/Source/Mods/AncientUrbanRuins.cs b/Source/Mods/AncientUrbanRuins.cs index 904a438..aad4e32 100644 --- a/Source/Mods/AncientUrbanRuins.cs +++ b/Source/Mods/AncientUrbanRuins.cs @@ -1,6 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; using System.Reflection.Emit; using HarmonyLib; +using Multiplayer.API; +using RimWorld; using RimWorld.Planet; using Verse; @@ -11,6 +16,20 @@ namespace Multiplayer.Compat; [MpCompatFor("XMB.AncientUrbanrUins.MO")] public class AncientUrbanRuins { + #region Fields + + // GameComponent_AncientMarket + private static Type ancientMarketGameCompType; + private static FastInvokeHandler ancientMarketGameCompGetScheduleMethod; + private static AccessTools.FieldRef ancientMarketGameCompSchedulesField; + // LevelSchedule + private static AccessTools.FieldRef levelScheduleAllowedLevelsField; + private static AccessTools.FieldRef> levelScheduleTimeScheduleField; + // MapParent_Custom + private static AccessTools.FieldRef customMapEntranceField; + + #endregion + #region Main patch public AncientUrbanRuins(ModContentPack mod) @@ -18,6 +37,7 @@ public AncientUrbanRuins(ModContentPack mod) // Mod uses 3 different assemblies, 2 of them use the same namespace. MpCompatPatchLoader.LoadPatch(this); + MpSyncWorkers.Requires(); #region RNG @@ -53,6 +73,30 @@ public AncientUrbanRuins(ModContentPack mod) } #endregion + + #region Permitted floors timetable + + { + // Prepare stuff + var type = ancientMarketGameCompType = AccessTools.TypeByName("AncientMarket_Libraray.GameComponent_AncientMarket"); + ancientMarketGameCompGetScheduleMethod = MethodInvoker.GetHandler(AccessTools.DeclaredMethod(type, "GetSchedule")); + ancientMarketGameCompSchedulesField = AccessTools.FieldRefAccess(type, "schedules"); + + type = AccessTools.TypeByName("AncientMarket_Libraray.LevelSchedule"); + levelScheduleAllowedLevelsField = AccessTools.FieldRefAccess(type, "allowedLevels"); + levelScheduleTimeScheduleField = AccessTools.FieldRefAccess>(type, "timeSchedule"); + + customMapEntranceField = AccessTools.FieldRefAccess("AncientMarket_Libraray.MapParent_Custom:entrance"); + + // Add to allowed (2), remove from allowed (4) + MpCompat.RegisterLambdaDelegate( + "AncientMarket_Libraray.Window_AllowLevel", + nameof(Window.DoWindowContents), + ["schedule"], // Skip x and y, syncing them is not needed - they're only used for UI + 2, 4); + } + + #endregion } #endregion @@ -101,4 +145,113 @@ private static void SyncedDestroySite(WorldObject site) } #endregion + + #region Permitted floors timetable patches and syncing + + [MpCompatSyncWorker("AncientMarket_Libraray.LevelSchedule")] + private static void SyncLevelSchedule(SyncWorker sync, ref object schedule) + { + var comp = Current.Game.GetComponent(ancientMarketGameCompType); + + if (sync.isWriting) + { + if (schedule == null) + { + sync.Write(null); + return; + } + + // Get the dictionary of all schedules and pawns and iterate over them + var list = ancientMarketGameCompSchedulesField(comp); + Pawn pawn = null; + foreach (DictionaryEntry value in list) + { + // If the value is the schedule we're syncing, sync the pawn key. + if (value.Value == schedule) + { + pawn = value.Key as Pawn; + break; + } + } + + sync.Write(pawn); + } + else + { + var pawn = sync.Read(); + // Will create the schedule if null here, as it may be created in interface. + if (pawn != null) + schedule = ancientMarketGameCompGetScheduleMethod(comp, pawn); + } + } + + [MpCompatPrefix("AncientMarket_Libraray.Window_AllowLevel", nameof(Window.DoWindowContents), 2)] + private static bool PreMapAddedToSchedule(PocketMapParent m, object ___schedule) + { + if (!MP.IsInMultiplayer || !MP.IsExecutingSyncCommand) + return true; + // Hopefully shouldn't happen + if (m == null || ___schedule == null) + return false; + + var allowedLevels = levelScheduleAllowedLevelsField(___schedule); + var entrance = customMapEntranceField(m); + + // If the allowed levels already contains the entrance, cancel execution. + return !allowedLevels.Contains(entrance); + } + + [MpCompatSyncMethod(cancelIfAnyArgNull = true)] + private static void SyncedSetTimeAssignment(Pawn pawn, int hour, bool allow) + { + // No need to check if hour is correct, as it should be. + var comp = Current.Game.GetComponent(ancientMarketGameCompType); + var schedule = ancientMarketGameCompGetScheduleMethod(comp, pawn); + levelScheduleTimeScheduleField(schedule)[hour] = allow; + } + + private static void ReplacedSetTimeSchedule(List schedule, int hour, bool allow, Pawn pawn) + { + // Ignore execution if there would be no change, prevents unnecessary syncing. + if (schedule[hour] != allow) + SyncedSetTimeAssignment(pawn, hour, allow); + } + + [MpCompatTranspiler("AncientMarket_Libraray.PawnColumnWorker_LevelTimetable", "DoTimeAssignment")] + private static IEnumerable ReplaceIndexerSetterWithSyncedTimetableChange(IEnumerable instr, MethodBase baseMethod) + { + // The method calls (List)[int] = bool. We need to sync this call, which happens + // after a check if the cell was clicked. We replace the call to this setter, replacing + // it with our own method. We also need to get a pawn for syncing, as we can't just + // sync List here - we need to sync the Pawn or LevelSchedule. + + var target = AccessTools.DeclaredIndexerSetter(typeof(List<>).MakeGenericType(typeof(bool)), [typeof(int)]); + var replacement = MpMethodUtil.MethodOf(ReplacedSetTimeSchedule); + var replacedCount = 0; + + foreach (var ci in instr) + { + if (ci.Calls(target)) + { + // Push the Pawn argument onto the stack + yield return new CodeInstruction(OpCodes.Ldarg_2); + + ci.opcode = OpCodes.Call; + ci.operand = replacement; + + replacedCount++; + } + + yield return ci; + } + + const int expected = 1; + if (replacedCount != expected) + { + var name = (baseMethod.DeclaringType?.Namespace).NullOrEmpty() ? baseMethod.Name : $"{baseMethod.DeclaringType!.Name}:{baseMethod.Name}"; + Log.Warning($"Patched incorrect number of Find.CameraDriver.MapPosition calls (patched {replacedCount}, expected {expected}) for method {name}"); + } + } + + #endregion } \ No newline at end of file diff --git a/Source/MpSyncWorkers.cs b/Source/MpSyncWorkers.cs index 076a912..f8c3278 100644 --- a/Source/MpSyncWorkers.cs +++ b/Source/MpSyncWorkers.cs @@ -1,18 +1,38 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using HarmonyLib; using Multiplayer.API; using RimWorld; +using RimWorld.Planet; using Verse; namespace Multiplayer.Compat { public static class MpSyncWorkers { + private static readonly HashSet AlreadyRegistered = []; + public static void Requires() => Requires(typeof(T)); public static void Requires(Type type) { + // Registering the same sync worker multiple times would result in + // the warning about sync worker existing in MP. Store a list of + // sync workers we registered to avoid the warning if we registered + // it, as well as prevent duplicate warnings if the sync worker exists + // in MP already, and we call this method multiple times for the same type. + if (!AlreadyRegistered.Add(type)) + return; + + // HasSyncWorker would return true, since MP has an implicit sync worker for + // WorldObject, but it currently cannot handle WorldObject (fixed by PR #504). + if (type == typeof(PocketMapParent)) + { + MP.RegisterSyncWorker(SyncPocketMapParent, isImplicit: true); + return; + } + if (HasSyncWorker(type)) { Log.Warning($"Sync worker of type {type} already exists in MP, temporary sync worker can be removed from MP Compat"); @@ -108,6 +128,25 @@ private static void SyncDesignationManager(SyncWorker sync, ref DesignationManag manager = sync.Read().designationManager; } + private static void SyncPocketMapParent(SyncWorker sync, ref PocketMapParent pmp) + { + if (sync.isWriting) + { + // This will sync ID for PocketMapParent twice, since it'll also use + // the sync worker for WorldObject first. However, that sync worker + // will fail as it doesn't support pocket maps yet (fixed by PR #504). + sync.Write(pmp?.ID ?? -1); + } + else + { + var id = sync.Read(); + // Skip if the pocket map is null. Also make sure to not + // overwrite the object if it happens to not be null. + if (id != -1) + pmp ??= Find.World.pocketMaps.Find(p => p.ID == id); + } + } + private static bool HasSyncWorker(Type type) { const string fieldPath = "Multiplayer.Client.Multiplayer:serialization";