diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 15987171..a0c35a87 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -581,13 +581,4 @@ static void FixStorage(IStoreSettingsParent __instance, StorageSettings ___allow ___allowedNutritionSettings.owner ??= __instance; } } - - [HarmonyPatch(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.TryAssign))] - static class Patc - { - static IEnumerable Transpiler(IEnumerable insts) - { - return insts; - } - } } diff --git a/Source/Client/Syncing/Dict/SyncDictDlc.cs b/Source/Client/Syncing/Dict/SyncDictDlc.cs index 4ee131b2..ce91639e 100644 --- a/Source/Client/Syncing/Dict/SyncDictDlc.cs +++ b/Source/Client/Syncing/Dict/SyncDictDlc.cs @@ -194,7 +194,7 @@ public static class SyncDictDlc WriteSync(data, dialog.ritual); }, (ByteReader data) => { - var assignments = ReadSync(data) as MpRitualAssignments; + var assignments = (MpRitualAssignments)ReadSync(data); if (assignments == null) return null; var ritual = ReadSync(data); // todo handle ritual becoming null? @@ -208,7 +208,7 @@ public static class SyncDictDlc WriteSync(data, dialog.ritual); }, (ByteReader data) => { - var assignment = ReadSync(data) as MpRitualAssignments; + var assignment = (MpRitualAssignments)ReadSync(data); if (assignment == null) return null; var ritual = ReadSync(data); // todo handle ritual becoming null? diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 75e8e013..b145fc5b 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -7,6 +7,7 @@ using System.Reflection.Emit; using HarmonyLib; using Multiplayer.Client.Patches; +using MultiplayerLoader; using Verse; namespace Multiplayer.Client @@ -266,44 +267,27 @@ private static void InitRituals() SyncDelegate.Lambda(typeof(SocialCardUtility), nameof(SocialCardUtility.DrawPawnRoleSelection), 0); // Begin role change: remove role SyncDelegate.Lambda(typeof(SocialCardUtility), nameof(SocialCardUtility.DrawPawnRoleSelection), 3); // Begin role change: assign role - // SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 0); // Select role: none - // SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 3); // Select role, set confirm text - // SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 4); // Select role, no confirm text + SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 0); // Select role: none + SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 3); // Select role, set confirm text + SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 4); // Select role, no confirm text /* - Ritual dialog + PawnRoleSelectionWidgetBase The UI's main interaction area is split into three types of groups of pawns. Each has three action handlers: (drop), (leftclick), (rightclick) The names in parenths below indicate what is synced for each handler. : - (Zero or more) roles: (local TryAssignReplace, local TryAssign), (null), (delegate) - Spectators: (assgn.TryAssignSpectate), (local TryAssignAnyRole), (assgn.RemoveParticipant) - Not participating: (assgn.RemoveParticipant), (delegate), float menus: (assgn.TryAssignSpectate, local TryAssignReplace, local TryAssign) + (Zero or more) roles: (TryAssignReplace, TryAssign), (null), (delegate) + Spectators: (assgn.TryAssignSpectate), (TryAssignAnyRole), (assgn.RemoveParticipant) + Not participating: (assgn.RemoveParticipant), (delegate), float menus: (assgn.TryAssignSpectate, TryAssignReplace, TryAssign) */ - var ritualRolesSerializer = Serializer.New( - (IEnumerable roles, object target, object[] _) => - { - var roleSelectionWidget = (PawnRitualRoleSelectionWidget)target; - return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); - }, - data => data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key) - ); - - // todo for 1.5 - // SyncMethod.Register(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace)) - // .TransformArgument(1, ritualRolesSerializer); - // SyncMethod.Register(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole)); - // SyncMethod.Register(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.TryAssign)) - // .TransformArgument(1, ritualRolesSerializer); - // - // SyncMethod.Lambda(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), 7); // Roles right click delegate (try assign spectate) - // SyncMethod.Lambda(typeof(PawnRoleSelectionWidgetBase), nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), 2); // Not participating left click delegate (try assign any role or spectate) - SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.TryAssignSpectate)); SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.RemoveParticipant)); + + SyncRituals.ApplyPrepatches(null); } private static void InitChoiceLetters() diff --git a/Source/Client/Syncing/Handler/SyncMethod.cs b/Source/Client/Syncing/Handler/SyncMethod.cs index 20e6a6c0..c10dc7ba 100644 --- a/Source/Client/Syncing/Handler/SyncMethod.cs +++ b/Source/Client/Syncing/Handler/SyncMethod.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Multiplayer.Client.Util; +using MultiplayerLoader; using Verse; namespace Multiplayer.Client @@ -14,7 +15,7 @@ public record SyncTransformer(Type LiveType, Type NetworkType, Delegate Writer, public delegate void SyncMethodWriter(object obj, SyncType type, string debugInfo); - public class SyncMethod : SyncHandler, ISyncMethod + public class SyncMethod : SyncHandler, ISyncMethod, ISyncMethodForPrepatcher { public readonly Type targetType; public readonly MethodInfo method; @@ -37,6 +38,8 @@ public class SyncMethod : SyncHandler, ISyncMethod private Action beforeCall; private Action afterCall; + public Type TargetType => targetType; + public SyncMethod(Type targetType, string instancePath, MethodInfo method, SyncType[] inTypes) { this.method = method; diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index 5a7d3181..e9464855 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -6,7 +6,7 @@ namespace Multiplayer.Client; internal static class ImplSerialization { - public static void Init(RwSyncTypeHelper typeHelper) + public static void Init() { Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IStoreSettingsParent)); Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IStorageGroupMember)); @@ -19,9 +19,5 @@ public static void Init(RwSyncTypeHelper typeHelper) Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IThingHolder)); Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IReloadableComp)); Multiplayer.serialization.RegisterForSyncWithImpl(typeof(Policy)); - - // todo for 1.5 - Multiplayer.serialization.RegisterForSyncWithImpl(typeof(PawnRoleSelectionWidgetBase)); - typeHelper.AddImplManually(typeof(PawnRoleSelectionWidgetBase), typeof(PawnRitualRoleSelectionWidget)); } } diff --git a/Source/Client/Syncing/RwSerialization.cs b/Source/Client/Syncing/RwSerialization.cs index 56f54899..840f78ea 100644 --- a/Source/Client/Syncing/RwSerialization.cs +++ b/Source/Client/Syncing/RwSerialization.cs @@ -16,8 +16,7 @@ public static class RwSerialization { public static void Init() { - var typeHelper = new RwSyncTypeHelper(); - Multiplayer.serialization = new Common.SyncSerialization(typeHelper); + Multiplayer.serialization = new Common.SyncSerialization(new RwSyncTypeHelper()); // CanHandle hooks Multiplayer.serialization.AddCanHandleHook(syncType => @@ -113,7 +112,7 @@ public static void Init() } ); - ImplSerialization.Init(typeHelper); + ImplSerialization.Init(); CompSerialization.Init(); ApiSerialization.Init(); DefSerialization.Init(); diff --git a/Source/Client/Syncing/RwSyncTypeHelper.cs b/Source/Client/Syncing/RwSyncTypeHelper.cs index dd1c2b0e..a245fd58 100644 --- a/Source/Client/Syncing/RwSyncTypeHelper.cs +++ b/Source/Client/Syncing/RwSyncTypeHelper.cs @@ -7,18 +7,8 @@ namespace Multiplayer.Client; internal class RwSyncTypeHelper : SyncTypeHelper { - private Dictionary> manualImplTypes = new(); - - public void AddImplManually(Type baseType, Type implType) - { - manualImplTypes.GetOrAddNew(baseType).Add(implType); - } - public override List GetImplementations(Type baseType) { - if (manualImplTypes.TryGetValue(baseType, out var manualImplType)) - return manualImplType; - return baseType.IsInterface ? TypeCache.interfaceImplementationsOrdered[baseType] : TypeCache.subClassesOrdered[baseType]; diff --git a/Source/Client/Syncing/Sync.cs b/Source/Client/Syncing/Sync.cs index e62706ed..22829e65 100644 --- a/Source/Client/Syncing/Sync.cs +++ b/Source/Client/Syncing/Sync.cs @@ -31,16 +31,10 @@ public static void PostInitHandlers() public static SyncMethod Method(Type targetType, string methodName, SyncType[] argTypes = null) { - return Method(targetType, null, methodName, argTypes); - } - - public static SyncMethod Method(Type targetType, string instancePath, string methodName, SyncType[] argTypes = null) - { - var instanceType = instancePath == null ? targetType : MpReflection.PathType($"{targetType}/{instancePath}"); - var method = AccessTools.Method(instanceType, methodName, argTypes?.Select(t => t.type).ToArray()) - ?? throw new Exception($"Couldn't find method {instanceType}::{methodName}"); + var method = AccessTools.Method(targetType, methodName, argTypes?.Select(t => t.type).ToArray()) + ?? throw new Exception($"Couldn't find method {targetType}::{methodName}"); - SyncMethod handler = new SyncMethod(targetType, instancePath, method, argTypes); + SyncMethod handler = new SyncMethod(targetType, null, method, argTypes); handlers.Add(handler); return handler; } diff --git a/Source/Client/Syncing/SyncTemplates.cs b/Source/Client/Syncing/SyncTemplates.cs index e07faffb..88a379f9 100644 --- a/Source/Client/Syncing/SyncTemplates.cs +++ b/Source/Client/Syncing/SyncTemplates.cs @@ -30,10 +30,10 @@ static IEnumerable Transpiler(MethodBase original, IEnumerable< { int idx; var label = gen.DefineLabel(); - var parameter = original.GetParameters(); + var parameters = original.GetParameters(); idx = 0; - foreach (var pInfo in parameter) { + foreach (var pInfo in parameters) { var argIndex = idx++ + (original.IsStatic ? 0 : 1); var pType = pInfo.ParameterType; if (pInfo.IsOut) { @@ -58,12 +58,12 @@ static IEnumerable Transpiler(MethodBase original, IEnumerable< yield return new CodeInstruction(OpCodes.Ldc_I4, Sync.methodBaseToInternalId[original]); yield return new CodeInstruction(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0); - yield return new CodeInstruction(OpCodes.Ldc_I4, parameter.Length); + yield return new CodeInstruction(OpCodes.Ldc_I4, parameters.Length); yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); idx = 0; var arrayIdx = 0; - foreach (var pInfo in parameter) { + foreach (var pInfo in parameters) { var argIndex = idx++ + (original.IsStatic ? 0 : 1); var pType = pInfo.ParameterType; yield return new CodeInstruction(OpCodes.Dup); @@ -116,7 +116,6 @@ static IEnumerable CreateDefaultCodes(ILGenerator generator, Ty yield return new CodeInstruction(OpCodes.Ldc_I8, (long) 0); else yield return new CodeInstruction(OpCodes.Ldc_I4, 0); - yield break; } } diff --git a/Source/Client/Util/Extensions.cs b/Source/Client/Util/Extensions.cs index 252ed092..fb6ed69e 100644 --- a/Source/Client/Util/Extensions.cs +++ b/Source/Client/Util/Extensions.cs @@ -253,20 +253,6 @@ public static string[] Names(this ParameterInfo[] pinfo) return pinfo.Select(pi => pi.Name).ToArray(); } - public static string After(this string s, char c) - { - if (s.IndexOf(c) == -1) - throw new ArgumentException($"Char {c} not found in string {s}"); - return s.Substring(s.IndexOf(c) + 1); - } - - public static string Until(this string s, char c) - { - if (s.IndexOf(c) == -1) - throw new ArgumentException($"Char {c} not found in string {s}"); - return s.Substring(0, s.IndexOf(c)); - } - public static string CamelSpace(this string str) { var builder = new StringBuilder(); diff --git a/Source/MultiplayerLoader/ExternalAnnotations/0PrepatcherAPI/Annotations.xml b/Source/MultiplayerLoader/ExternalAnnotations/0PrepatcherAPI/Annotations.xml new file mode 100644 index 00000000..1c6ed434 --- /dev/null +++ b/Source/MultiplayerLoader/ExternalAnnotations/0PrepatcherAPI/Annotations.xml @@ -0,0 +1,7 @@ + + + + 3 + + + diff --git a/Source/MultiplayerLoader/ISyncMethodForPrepatcher.cs b/Source/MultiplayerLoader/ISyncMethodForPrepatcher.cs new file mode 100644 index 00000000..7146a3f5 --- /dev/null +++ b/Source/MultiplayerLoader/ISyncMethodForPrepatcher.cs @@ -0,0 +1,9 @@ +using System; +using Multiplayer.API; + +namespace MultiplayerLoader; + +public interface ISyncMethodForPrepatcher : ISyncMethod +{ + Type TargetType { get; } +} diff --git a/Source/Client/Util/MpMethodUtil.cs b/Source/MultiplayerLoader/MpMethodUtil.cs similarity index 93% rename from Source/Client/Util/MpMethodUtil.cs rename to Source/MultiplayerLoader/MpMethodUtil.cs index a353ff00..8a94ef60 100644 --- a/Source/Client/Util/MpMethodUtil.cs +++ b/Source/MultiplayerLoader/MpMethodUtil.cs @@ -122,7 +122,7 @@ public static int GetMethodDebugId(MethodBase method) } catch (Exception e) { - throw new Exception($"Extracting debug id for {method.DeclaringType}::{method.Name} failed at {cur} with: {e.Message}"); + throw new Exception($"Extracting debug id for {method.DeclaringType}::{method.Name} failed at {cur} with: {e}"); } throw new Exception($"Couldn't determine debug id for parent method {method.DeclaringType}::{method.Name}"); @@ -171,5 +171,19 @@ public static MethodBase GetMethod(Type type, string methodName, MethodType meth return null; } + + public static string After(this string s, char c) + { + if (s.IndexOf(c) == -1) + throw new ArgumentException($"Char {c} not found in string {s}"); + return s.Substring(s.IndexOf(c) + 1); + } + + public static string Until(this string s, char c) + { + if (s.IndexOf(c) == -1) + throw new ArgumentException($"Char {c} not found in string {s}"); + return s.Substring(0, s.IndexOf(c)); + } } } diff --git a/Source/MultiplayerLoader/Prepatches.cs b/Source/MultiplayerLoader/Prepatches.cs index fe0dc1fb..3261b549 100644 --- a/Source/MultiplayerLoader/Prepatches.cs +++ b/Source/MultiplayerLoader/Prepatches.cs @@ -1,11 +1,15 @@ -using Mono.Cecil; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Mono.Cecil; using Mono.Cecil.Cil; using MonoMod.Utils; using Multiplayer.Common; using Prepatcher; using RimWorld; -namespace Multiplayer.Client; +namespace MultiplayerLoader; public static class Prepatches { @@ -15,10 +19,65 @@ static void DontSetResolution(ModuleDefinition module) if (MpVersion.IsDebug) { var resolutionUtilityType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); - resolutionUtilityType.FindMethod("SetResolutionRaw").Body.Instructions.Insert( + resolutionUtilityType.FindMethod(nameof(ResolutionUtility.SetResolutionRaw))!.Body.Instructions.Insert( 0, Instruction.Create(OpCodes.Ret) ); } } + + [FreePatch] + static void ApplySyncPrepatches(ModuleDefinition module) + { + SyncRituals.ApplyPrepatches(module); + } + + internal static void AddPrepatcherPrefix(ModuleDefinition module, int id, MethodInfo target, MethodInfo prefix) + { + var targetMethod = module.ImportReference(target).Resolve(); + var firstInst = targetMethod.Body.Instructions.First(); + + IEnumerable PrefixInstructions() + { + yield return Instruction.Create(OpCodes.Ldc_I4, id); // Load index + yield return Instruction.Create(OpCodes.Ldarg_0); // Load __instance + + // Create __args array + yield return Instruction.Create(OpCodes.Ldc_I4, target.GetParameters().Length); + yield return Instruction.Create(OpCodes.Newarr, module.TypeSystem.Object); + + int idx = 0; + foreach (var pInfo in target.GetParameters()) + { + var pType = pInfo.ParameterType; + + yield return Instruction.Create(OpCodes.Dup); + yield return Instruction.Create(OpCodes.Ldc_I4, idx); + yield return Instruction.Create(OpCodes.Ldarg, targetMethod.Parameters.ElementAt(idx)); + + if (pType.IsByRef) + throw new Exception("Byrefs aren't handled"); + if (pType.IsValueType) + yield return Instruction.Create(OpCodes.Box, module.ImportReference(pType)); + + yield return Instruction.Create(OpCodes.Stelem_Ref); + + idx++; + } + + yield return Instruction.Create(OpCodes.Call, module.ImportReference(prefix)); + yield return Instruction.Create(OpCodes.Brtrue, firstInst); + + if (target.ReturnType == typeof(bool)) + yield return Instruction.Create(OpCodes.Ldc_I4_0); + else if (target.ReturnType == typeof(void)) + yield return Instruction.Create(OpCodes.Nop); + else + throw new Exception($"Can't handle return type {target.ReturnType}"); + + yield return Instruction.Create(OpCodes.Ret); + } + + targetMethod.Body.Instructions.InsertRange(0, PrefixInstructions()); + } } diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs new file mode 100644 index 00000000..b927f4bd --- /dev/null +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Mono.Cecil; +using Multiplayer.API; +using Multiplayer.Client.Util; +using RimWorld; + +namespace MultiplayerLoader; + +public static class SyncRituals +{ + private static List syncMethodsWithPrepatches = new(); + + public static bool PrepatcherPrefix(int index, object __instance, object[] __args) + { + // This prefix is applied to a generic method and called by all possible instantiations + // This check makes sure the type is correct + if (syncMethodsWithPrepatches[index].TargetType == __instance.GetType()) + return !syncMethodsWithPrepatches[index].DoSync(__instance, __args); + return true; + } + + private static MethodInfo syncMethod = AccessTools.Method("Multiplayer.Client.Sync:Method"); + + // This method is called twice (two passes) + // The first pass with module != null is called from Prepatcher and applies prepatches + // The second pass with module == null is called from Sync initialization and registers SyncMethods + public static void ApplyPrepatches(ModuleDefinition module) + { + void Register(Type baseType, string method, Type derivedType, Action postProcess = null) + { + if (module != null) + { + Prepatches.AddPrepatcherPrefix( + module, + syncMethodsWithPrepatches.Count, + AccessTools.Method(baseType, method), + AccessTools.Method(typeof(SyncRituals), nameof(PrepatcherPrefix)) + ); + + // The ids need to be in sync between passes (note the array is recreated after Prepatcher restarts the game) + syncMethodsWithPrepatches.Add(null); + } + else + { + // Sync.Method doesn't apply a Harmony patch and doesn't require the DeclaringType to be the target type + var syncMethod = (ISyncMethodForPrepatcher)SyncRituals.syncMethod.Invoke(null, [derivedType, method, null]); + postProcess?.Invoke(syncMethod); + syncMethodsWithPrepatches.Add(syncMethod); + } + } + + // ======= PawnRitualRoleSelectionWidget ======= + + var ritualRolesSerializer = Serializer.New( + (IEnumerable roles, object target, object[] _) => + { + var roleSelectionWidget = (PawnRitualRoleSelectionWidget)target; + return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); + }, + data => data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key) + ); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnRitualRoleSelectionWidget)); + + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnRitualRoleSelectionWidget), + m => m.TransformArgument(1, ritualRolesSerializer)); + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) + + Register( + typeof(PawnRoleSelectionWidgetBase<>), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + } +}