Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Ancient urban ruins compat #468

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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