Skip to content

Commit

Permalink
Update Ancient urban ruins compat (#468)
Browse files Browse the repository at this point in the history
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<Type>` 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.
  • Loading branch information
SokyranTheDragon authored Sep 2, 2024
1 parent 7faabda commit 51e86ac
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 1 deletion.
155 changes: 154 additions & 1 deletion Source/Mods/AncientUrbanRuins.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,13 +16,28 @@ 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<GameComponent, IDictionary> ancientMarketGameCompSchedulesField;
// LevelSchedule
private static AccessTools.FieldRef<object, IList> levelScheduleAllowedLevelsField;
private static AccessTools.FieldRef<object, List<bool>> levelScheduleTimeScheduleField;
// MapParent_Custom
private static AccessTools.FieldRef<PocketMapParent, MapPortal> customMapEntranceField;

#endregion

#region Main patch

public AncientUrbanRuins(ModContentPack mod)
{
// Mod uses 3 different assemblies, 2 of them use the same namespace.

MpCompatPatchLoader.LoadPatch(this);
MpSyncWorkers.Requires<PocketMapParent>();

#region RNG

Expand Down Expand Up @@ -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<IDictionary>(type, "schedules");

type = AccessTools.TypeByName("AncientMarket_Libraray.LevelSchedule");
levelScheduleAllowedLevelsField = AccessTools.FieldRefAccess<IList>(type, "allowedLevels");
levelScheduleTimeScheduleField = AccessTools.FieldRefAccess<List<bool>>(type, "timeSchedule");

customMapEntranceField = AccessTools.FieldRefAccess<MapPortal>("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
Expand Down Expand Up @@ -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<Pawn>(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<Pawn>();
// 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<bool> 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<CodeInstruction> ReplaceIndexerSetterWithSyncedTimetableChange(IEnumerable<CodeInstruction> instr, MethodBase baseMethod)
{
// The method calls (List<bool>)[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<bool> 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
}
39 changes: 39 additions & 0 deletions Source/MpSyncWorkers.cs
Original file line number Diff line number Diff line change
@@ -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<Type> AlreadyRegistered = [];

public static void Requires<T>() => 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<PocketMapParent>(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");
Expand Down Expand Up @@ -108,6 +128,25 @@ private static void SyncDesignationManager(SyncWorker sync, ref DesignationManag
manager = sync.Read<Map>().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<int>();
// 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";
Expand Down

0 comments on commit 51e86ac

Please sign in to comment.