Skip to content

Commit 31208da

Browse files
Fix #521 (#527)
Fixing the issue of multiple actions calling the same method was (partially) fixed by syncing both the hashed method's description and the hashed original target's type description. This only fixes the issue partially, as it will only work if the type we're syncing is a generic type and each possible action uses a different generic arguments. However, it should be enough to handle everything in the game, as well as most (if not all) mods. As for fixing the issue where an action we sync is opening a confirmation dialog - the fix is a bit more complex. I've added a new delegate that will work as a wrapper around the original action. It may return either null or the synced method as-is, in which case nothing will happen. However, the wrapper may return a different action that will be called instead of immediately syncing the method. The wrapper is passed the current instance of the object, its arguments, the original `Action` that was going to be called, as well as the `Action` that, when invoked, will sync the data. The arguments are (currently) unused, but I've decided to include them for the case of future-proofing this code. As for the wrappers I've included: The wrapper for Caravan actions will return null (no wrapper) unless the method's declaring type is `CaravanArrivalActionUtility.<>c__DisplayClass0_1<T>`. If the type matches then we will replace the original action with our own which will access the `action` field from the target type, and: - If we're in a synced call, it'll execute that action. - If not in a synced call it'll replace the action with our syncing action and call the original method, displaying the confirmation dialog (which, if accepted, will sync the call). As for the wrapper for Transport Pods, it will return null (no wrapper) unless the method's declaring type is `TransportPodsArrivalActionUtility.<>c__DisplayClass0_0<T>`. If the type matches then we will replace the original action with our own which will access the `uiConfirmationCallback` field. - If executing commands, we set the confirmation to null and call the original method - This is done so the original method calls the actual action without the confirmation dialog - If not executing commands and the field is null, just sync the call - If not executing commands and the field is not null, call the confirmation callback with our syncing action as the action we pass to it As an additional note, we access the inner compiler-generated types directly. This is due to `MpMethodUtil` not supporting generic types and methods. If we decide to make it support those, we'll need to change how we access those types.
1 parent d64091d commit 31208da

File tree

2 files changed

+116
-14
lines changed

2 files changed

+116
-14
lines changed

Source/Client/Syncing/Game/SyncActions.cs

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Reflection;
6+
using HarmonyLib;
67
using Verse;
78

89
namespace Multiplayer.Client
@@ -12,23 +13,96 @@ static class SyncActions
1213
static SyncAction<FloatMenuOption, WorldObject, Caravan, object> SyncWorldObjCaravanMenus;
1314
static SyncAction<FloatMenuOption, WorldObject, IEnumerable<IThingHolder>, CompLaunchable> SyncTransportPodMenus;
1415

16+
static Type CaravanActionConfirmationType;
17+
static Type TransportPodActionConfirmationType;
18+
1519
public static void Init()
1620
{
17-
SyncWorldObjCaravanMenus = RegisterActions((WorldObject obj, Caravan c) => obj.GetFloatMenuOptions(c), o => ref o.action);
21+
void Error(string error)
22+
{
23+
Multiplayer.loadingErrors = true;
24+
Log.Error(error);
25+
}
26+
27+
// TODO: Use MpMethodUtil instead if we decide to make it work with generic types/methods (already in MP Compat, so use it). Or remove this TODO if we decide not to.
28+
CaravanActionConfirmationType = AccessTools.Inner(typeof(CaravanArrivalActionUtility), "<>c__DisplayClass0_1`1");
29+
TransportPodActionConfirmationType = AccessTools.Inner(typeof(TransportPodsArrivalActionUtility), "<>c__DisplayClass0_0`1");
30+
31+
if (CaravanActionConfirmationType == null) Error($"Could not find type: {nameof(CaravanArrivalActionUtility)}.<>c__DisplayClass0_1<T>");
32+
if (TransportPodActionConfirmationType == null) Error($"Could not find type: {nameof(TransportPodsArrivalActionUtility)}.<>c__DisplayClass0_0<T>");
33+
34+
SyncWorldObjCaravanMenus = RegisterActions((WorldObject obj, Caravan c) => obj.GetFloatMenuOptions(c), ActionGetter, WorldObjectCaravanMenuWrapper);
1835
SyncWorldObjCaravanMenus.PatchAll(nameof(WorldObject.GetFloatMenuOptions));
1936

20-
SyncTransportPodMenus = RegisterActions((WorldObject obj, IEnumerable<IThingHolder> p, CompLaunchable r) => obj.GetTransportPodsFloatMenuOptions(p, r), o => ref o.action);
37+
SyncTransportPodMenus = RegisterActions((WorldObject obj, IEnumerable<IThingHolder> p, CompLaunchable r) => obj.GetTransportPodsFloatMenuOptions(p, r), o => ref o.action, TransportPodMenuWrapper);
2138
SyncTransportPodMenus.PatchAll(nameof(WorldObject.GetTransportPodsFloatMenuOptions));
2239
}
2340

24-
static SyncAction<T, A, B, object> RegisterActions<T, A, B>(Func<A, B, IEnumerable<T>> func, ActionGetter<T> actionGetter)
41+
private static ref Action ActionGetter(FloatMenuOption o) => ref o.action;
42+
43+
private static Action WorldObjectCaravanMenuWrapper(FloatMenuOption instance, WorldObject a, Caravan b, object c, Action original, Action sync)
44+
{
45+
if (instance.action.Method.DeclaringType is not { IsGenericType: true })
46+
return null;
47+
48+
if (instance.action.Method.DeclaringType.GetGenericTypeDefinition() != CaravanActionConfirmationType)
49+
return null;
50+
51+
return () =>
52+
{
53+
var field = AccessTools.DeclaredField(original.Target.GetType(), "action");
54+
if (!Multiplayer.ExecutingCmds)
55+
{
56+
// If not in a synced call then replace the method that will be
57+
// called by confirmation dialog with our synced method call.
58+
field.SetValue(original.Target, sync);
59+
original();
60+
return;
61+
}
62+
63+
// If we're in a synced call, just call the action itself (after confirmation dialog)
64+
((Action)field.GetValue(original.Target))();
65+
};
66+
}
67+
68+
public static Action TransportPodMenuWrapper(FloatMenuOption instance, WorldObject worldObject, IEnumerable<IThingHolder> thingHolders, CompLaunchable compLaunchable, Action original, Action sync)
69+
{
70+
if (instance.action.Method.DeclaringType is not { IsGenericType: true })
71+
return null;
72+
73+
if (instance.action.Method.DeclaringType.GetGenericTypeDefinition() != TransportPodActionConfirmationType)
74+
return null;
75+
76+
return () =>
77+
{
78+
var field = AccessTools.DeclaredField(original.Target.GetType(), "uiConfirmationCallback");
79+
80+
if (Multiplayer.ExecutingCmds)
81+
{
82+
// Remove UI confirmation during synced commands so the method is just called directly
83+
field.SetValue(original.Target, null);
84+
original();
85+
return;
86+
}
87+
88+
var confirmation = (Action<Action>)field.GetValue(original.Target);
89+
// If no confirmation dialog, just sync
90+
if (confirmation == null)
91+
sync();
92+
// If there's a confirmation dialog, call it with sync as its synced method
93+
else
94+
confirmation(sync);
95+
};
96+
}
97+
98+
static SyncAction<T, A, B, object> RegisterActions<T, A, B>(Func<A, B, IEnumerable<T>> func, ActionGetter<T> actionGetter, ActionWrapper<T, A, B, object> actionWrapper = null)
2599
{
26-
return RegisterActions<T, A, B, object>((a, b, c) => func(a, b), actionGetter);
100+
return RegisterActions<T, A, B, object>((a, b, c) => func(a, b), actionGetter, actionWrapper);
27101
}
28102

29-
static SyncAction<T, A, B, C> RegisterActions<T, A, B, C>(Func<A, B, C, IEnumerable<T>> func, ActionGetter<T> actionGetter)
103+
static SyncAction<T, A, B, C> RegisterActions<T, A, B, C>(Func<A, B, C, IEnumerable<T>> func, ActionGetter<T> actionGetter, ActionWrapper<T, A, B, C> actionWrapper = null)
30104
{
31-
var sync = new SyncAction<T, A, B, C>(func, actionGetter);
105+
var sync = new SyncAction<T, A, B, C>(func, actionGetter, actionWrapper);
32106
Sync.handlers.Add(sync);
33107

34108
return sync;

Source/Client/Syncing/Handler/SyncAction.cs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
namespace Multiplayer.Client
1111
{
12-
public delegate ref Action ActionGetter<T>(T t);
12+
public delegate ref Action ActionGetter<in T>(T t);
13+
public delegate Action ActionWrapper<in T, in A, in B, in C>(T instance, A a, B b, C c, Action original, Action syncAction);
1314

1415
public interface ISyncAction
1516
{
@@ -20,11 +21,13 @@ public class SyncAction<T, A, B, C> : SyncHandler, ISyncAction
2021
{
2122
private Func<A, B, C, IEnumerable<T>> func;
2223
private ActionGetter<T> actionGetter;
24+
private ActionWrapper<T, A, B, C> actionWrapper;
2325

24-
public SyncAction(Func<A, B, C, IEnumerable<T>> func, ActionGetter<T> actionGetter)
26+
public SyncAction(Func<A, B, C, IEnumerable<T>> func, ActionGetter<T> actionGetter, ActionWrapper<T, A, B, C> actionWrapper = null)
2527
{
2628
this.func = func;
2729
this.actionGetter = actionGetter;
30+
this.actionWrapper = actionWrapper ?? ((_, _, _, _, _, _) => null);
2831
}
2932

3033
public IEnumerable<T> DoSync(A target, B arg0, C arg1)
@@ -40,7 +43,9 @@ public IEnumerable<T> DoSync(A target, B arg0, C arg1)
4043
int j = i;
4144
i++;
4245
var original = actionGetter(t);
43-
actionGetter(t) = () => ActualSync(target, arg0, arg1, original);
46+
var sync = () => ActualSync(target, arg0, arg1, original);
47+
var wrapper = actionWrapper(t, target, arg0, arg1, original, sync);
48+
actionGetter(t) = wrapper ?? sync;
4449

4550
yield return t;
4651
}
@@ -68,12 +73,18 @@ private void ActualSync(A target, B arg0, C arg1, Action original)
6873
SyncSerialization.WriteSync(writer, arg0);
6974
SyncSerialization.WriteSync(writer, arg1);
7075

71-
writer.WriteInt32(GenText.StableStringHash(original.Method.MethodDesc()));
72-
Log.Message(original.Method.MethodDesc());
76+
var methodDesc = original.Method.MethodDesc();
77+
writer.Log.Node($"Method desc: {methodDesc}");
78+
writer.WriteInt32(GenText.StableStringHash(methodDesc));
79+
80+
// If target is null then just sync the hash for null
81+
var typeDesc = (original.Target?.GetType()).FullDescription();
82+
writer.Log.Node($"Type desc: {typeDesc}");
83+
writer.WriteInt32(GenText.StableStringHash(typeDesc));
7384

7485
int mapId = writer.MpContext().map?.uniqueID ?? -1;
7586

76-
writer.Log.Node("Map id: " + mapId);
87+
writer.Log.Node($"Map id: {mapId}");
7788
Multiplayer.WriterLog.AddCurrentNode(writer);
7889

7990
SendSyncCommand(mapId, writer);
@@ -85,9 +96,26 @@ public override void Handle(ByteReader data)
8596
B arg0 = SyncSerialization.ReadSync<B>(data);
8697
C arg1 = SyncSerialization.ReadSync<C>(data);
8798

88-
int descHash = data.ReadInt32();
99+
int methodDescHash = data.ReadInt32();
100+
int typeDescHash = data.ReadInt32();
101+
102+
var action = func(target, arg0, arg1)
103+
.Where(t =>
104+
{
105+
var a = actionGetter(t);
106+
// Match both the method description and target type description (including generics), or "null" string for the type
107+
return GenText.StableStringHash(a.Method.MethodDesc()) == methodDescHash &&
108+
GenText.StableStringHash((a.Target?.GetType()).FullDescription()) == typeDescHash;
109+
})
110+
.Select(t =>
111+
{
112+
var a = actionGetter(t);
113+
var w = actionWrapper(t, target, arg0, arg1, a, null);
114+
// Return the wrapper (if present) or the action itself
115+
return w ?? a;
116+
})
117+
.FirstOrDefault();
89118

90-
var action = func(target, arg0, arg1).Select(t => actionGetter(t)).FirstOrDefault(a => GenText.StableStringHash(a.Method.MethodDesc()) == descHash);
91119
action?.Invoke();
92120
}
93121

0 commit comments

Comments
 (0)