diff --git a/About/About.xml b/About/About.xml index f8070db9..e7c40d3a 100644 --- a/About/About.xml +++ b/About/About.xml @@ -7,8 +7,8 @@ RimWorld Multiplayer Team https://github.com/rwmt/Multiplayer - <color=red><b>Important: </b> This mod should be placed right below Core and expansions in the mod list to work properly! -Requires Rimworld >= v1.5.4034</color>\n + <b>Important: This mod should be placed right below Core and expansions in the mod list to work properly! +Requires RimWorld >= 1.5.4104</b>\n Multiplayer mod for RimWorld. FAQ - https://hackmd.io/@rimworldmultiplayer/docs/ diff --git a/Source/Client/AsyncTime/SetMapTime.cs b/Source/Client/AsyncTime/SetMapTime.cs index 28df1278..91c706b7 100644 --- a/Source/Client/AsyncTime/SetMapTime.cs +++ b/Source/Client/AsyncTime/SetMapTime.cs @@ -34,7 +34,7 @@ internal static void Prefix(ref TimeSnapshot? __state) } [HarmonyPriority(MpPriority.MpLast)] - internal static void Postfix(TimeSnapshot? __state) => __state?.Set(); + internal static void Finalizer(TimeSnapshot? __state) => __state?.Set(); } [HarmonyPatch] diff --git a/Source/Client/EarlyInit.cs b/Source/Client/EarlyInit.cs index 10f96a76..7e565f57 100644 --- a/Source/Client/EarlyInit.cs +++ b/Source/Client/EarlyInit.cs @@ -35,7 +35,7 @@ internal static void EarlyPatches(Harmony harmony) harmony.PatchMeasure( AccessTools.Constructor(typeof(Def), Type.EmptyTypes), new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Prefix)), - new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Postfix)) + finalizer: new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Finalizer)) ); Assembly.GetCallingAssembly().GetTypes().Do(type => diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 0f5af4ee..67cfd392 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -359,43 +359,6 @@ static void PlaySoundReplacement(SoundDef sound, Map map) } } -[HarmonyPatch] -static class DontClearDialogBeginRitualCache -{ - private static MethodInfo listClear = AccessTools.Method(typeof(List), "Clear"); - - static IEnumerable TargetMethods() - { - yield return typeof(Dialog_BeginRitual).GetConstructors(BindingFlags.Public | BindingFlags.Instance).First(); - } - - static IEnumerable Transpiler(IEnumerable insts, ILGenerator gen) - { - var list = insts.ToList(); - var brLabel = gen.DefineLabel(); - - foreach (var inst in list) - { - if (inst.operand == listClear) - { - yield return new CodeInstruction(OpCodes.Call, - AccessTools.Method(typeof(DontClearDialogBeginRitualCache), nameof(ShouldCancelCacheClear))); - yield return new CodeInstruction(OpCodes.Brfalse, brLabel); - yield return new CodeInstruction(OpCodes.Pop); - yield return new CodeInstruction(OpCodes.Ret); - yield return new CodeInstruction(OpCodes.Nop) { labels = { brLabel } }; - } - - yield return inst; - } - } - - static bool ShouldCancelCacheClear() - { - return Multiplayer.Ticking || Multiplayer.ExecutingCmds; - } -} - [HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] static class ApparelWornGraphicPathGetterPatch { diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index 1662ae95..a38b0088 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -32,7 +32,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index 076f251e..0eec307d 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -56,6 +56,7 @@ public MultiplayerGame() District.nextDistrictID = 1; Region.nextId = 1; ListerHaulables.groupCycleIndex = 0; + ListerHaulables.cellCycleIndices.Clear(); ZoneColorUtility.nextGrowingZoneColorIndex = 0; ZoneColorUtility.nextStorageZoneColorIndex = 0; diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 40b2b69d..96a5f662 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -337,7 +337,7 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po // Remove side effects from methods which are non-deterministic during ticking (e.g. camera dependent motes and sound effects) { var randPatchPrefix = new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Prefix)); - var randPatchPostfix = new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Postfix)); + var randPatchFinalizer = new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Finalizer)); var subSustainerStart = MpMethodUtil.GetLambda(typeof(SubSustainer), parentMethodType: MethodType.Constructor, parentArgs: new[] { typeof(Sustainer), typeof(SubSoundDef) }); var sampleCtor = typeof(Sample).GetConstructor(new[] { typeof(SubSoundDef) }); @@ -362,7 +362,7 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po var ritualMethods = new[] { canEverSpectate }; foreach (MethodBase m in effectMethods.Concat(moteMethods).Concat(fleckMethods).Concat(ritualMethods)) - TryPatch(m, randPatchPrefix, randPatchPostfix); + TryPatch(m, randPatchPrefix, finalizer: randPatchFinalizer); } SetCategory("Non-deterministic patches 2"); @@ -435,17 +435,17 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po // Set the map time for GUI methods depending on it { var setMapTimePrefix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), nameof(SetMapTimeForUI.Prefix))); - var setMapTimePostfix = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), nameof(SetMapTimeForUI.Postfix))); + var setMapTimeFinalizer = new HarmonyMethod(AccessTools.Method(typeof(SetMapTimeForUI), nameof(SetMapTimeForUI.Finalizer))); var windowMethods = new[] { "DoWindowContents", "WindowUpdate" }; foreach (string m in windowMethods) - TryPatch(typeof(MainTabWindow_Inspect).GetMethod(m), setMapTimePrefix, setMapTimePostfix); + TryPatch(typeof(MainTabWindow_Inspect).GetMethod(m), setMapTimePrefix, finalizer: setMapTimeFinalizer); foreach (var t in typeof(InspectTabBase).AllSubtypesAndSelf()) { var method = t.GetMethod("FillTab", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, Type.EmptyTypes, null); if (method != null && !method.IsAbstract) - TryPatch(method, setMapTimePrefix, setMapTimePostfix); + TryPatch(method, setMapTimePrefix, finalizer: setMapTimeFinalizer); } } } diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 5fbe2999..28ef9014 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -355,7 +355,7 @@ static void Postfix(WorldRoutePlanner __instance, ref bool __result) { if (Multiplayer.Client == null) return; - // Ignore pause + // Ignore unpausing if (__result && __instance.active && WorldRendererUtility.WorldRenderedNow) __result = false; } @@ -586,4 +586,13 @@ 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/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index 188b6d70..d4301500 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -140,7 +140,7 @@ public static void Prefix(ref bool __state) } [HarmonyPriority(MpPriority.MpLast)] - public static void Postfix(bool __state) + public static void Finalizer(bool __state) { if (__state) Rand.PopState(); diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index 510dfab2..18922d96 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -27,7 +27,7 @@ static void Postfix(bool __state, ref bool __result) GUI.color = Color.white; if (__result) { - CaravanFormingProxy.drawing.Session?.Remove(); + CaravanFormingProxy.drawing.Session?.Cancel(); __result = false; } } @@ -163,6 +163,7 @@ static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action o if (__instance.GetType() != typeof(Dialog_FormCaravan)) return; + // Handles showing the dialog from TimedForcedExit.CompTick -> TimedForcedExit.ForceReform if (Multiplayer.ExecutingCmds || Multiplayer.Ticking) { var comp = map.MpComp(); diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index b211d9c9..7f2639af 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -134,7 +134,12 @@ public void Reset() } [SyncMethod] - public void Remove() + public void Cancel() + { + Remove(); + } + + private void Remove() { map.MpComp().sessionManager.RemoveSession(this); Find.WorldRoutePlanner.Stop(); diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index fc5936ba..c5ebcc91 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -1,11 +1,10 @@ using HarmonyLib; using Multiplayer.API; using RimWorld; -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Multiplayer.Client.Util; +using System.Reflection.Emit; using UnityEngine; using Verse; using static Verse.Widgets; @@ -46,6 +45,7 @@ public void Start() public void OpenWindow(bool sound = true) { var dialog = new BeginRitualProxy( + data.assignments, data.ritualLabel, data.ritual, data.target, @@ -60,10 +60,7 @@ public void OpenWindow(bool sound = true) data.outcome, data.extraInfos, null - ) - { - assignments = data.assignments - }; + ); if (!sound) dialog.soundAppear = null; @@ -107,12 +104,43 @@ public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); - public BeginRitualProxy(string ritualLabel, Precept_Ritual ritual, TargetInfo target, Map map, ActionCallback action, Pawn organizer, RitualObligation obligation, PawnFilter filter = null, string okButtonText = null, List requiredPawns = null, Dictionary forcedForRole = null, RitualOutcomeEffectDef outcome = null, List extraInfoText = null, Pawn selectedPawn = null) : - base(ritualLabel, ritual, target, map, action, organizer, obligation, filter, okButtonText, requiredPawns, forcedForRole, outcome, extraInfoText, selectedPawn) + // In the base type, the unused fields are used to create RitualRoleAssignments which already exist in an MP session + // They are here to help keep the base constructor in sync with this one + public BeginRitualProxy( + RitualRoleAssignments assignments, + string ritualLabel, + Precept_Ritual ritual, + TargetInfo target, + Map map, + ActionCallback action, + Pawn organizer, + RitualObligation obligation, + PawnFilter filter = null, + string okButtonText = null, + // ReSharper disable once UnusedParameter.Local + List requiredPawns = null, + // ReSharper disable once UnusedParameter.Local + Dictionary forcedForRole = null, + RitualOutcomeEffectDef outcome = null, + List extraInfoText = null, + // ReSharper disable once UnusedParameter.Local + Pawn selectedPawn = null) : + base(assignments, ritual, target, ritual?.outcomeEffect?.def ?? outcome) { + this.obligation = obligation; + this.filter = filter; + this.organizer = organizer; + this.map = map; + this.action = action; + ritualExplanation = ritual?.ritualExplanation; + this.ritualLabel = ritualLabel; + this.okButtonText = okButtonText ?? "OK".Translate(); + extraInfos = extraInfoText; + soundClose = SoundDefOf.TabClose; // This gets cancelled in the base constructor if called from ticking/cmd in DontClearDialogBeginRitualCache + // Note that: cachedRoles is a static field, cachedRoles is only used for UI drawing cachedRoles.Clear(); if (ritual is { ideo: not null }) { @@ -199,15 +227,15 @@ static bool Prefix(Window window) && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let BeginRitualProxy through && (Multiplayer.ExecutingCmds || Multiplayer.Ticking)) { - var dialog = (Dialog_BeginRitual)window; - dialog.PostOpen(); // Completes initialization + var tempDialog = (Dialog_BeginRitual)window; + tempDialog.PostOpen(); // Completes initialization - var comp = dialog.map.MpComp(); + var comp = tempDialog.map.MpComp(); var session = comp.sessionManager.GetFirstOfType(); if (session != null && - (session.data.ritual != dialog.ritual || - session.data.outcome != dialog.outcome)) + (session.data.ritual != tempDialog.ritual || + session.data.outcome != tempDialog.outcome)) { Messages.Message("MpAnotherRitualInProgress".Translate(), MessageTypeDefOf.RejectInput, false); return false; @@ -217,16 +245,16 @@ static bool Prefix(Window window) { var data = new RitualData { - ritual = dialog.ritual, - target = dialog.target, - obligation = dialog.obligation, - outcome = dialog.outcome, - extraInfos = dialog.extraInfos, - action = dialog.action, - ritualLabel = dialog.ritualLabel, - confirmText = dialog.confirmText, - organizer = dialog.organizer, - assignments = MpUtil.ShallowCopy(dialog.assignments, new MpRitualAssignments()) + ritual = tempDialog.ritual, + target = tempDialog.target, + obligation = tempDialog.obligation, + outcome = tempDialog.outcome, + extraInfos = tempDialog.extraInfos, + action = tempDialog.action, + ritualLabel = tempDialog.ritualLabel, + confirmText = tempDialog.confirmText, + organizer = tempDialog.organizer, + assignments = MpUtil.ShallowCopy(tempDialog.assignments, new MpRitualAssignments()) }; session = comp.CreateRitualSession(data); @@ -242,6 +270,44 @@ static bool Prefix(Window window) } } + // Note that cachedRoles is a static field and is only used for UI drawing + [HarmonyPatch] + static class DontClearDialogBeginRitualCache + { + private static MethodInfo listClear = AccessTools.Method(typeof(List), "Clear"); + + static IEnumerable TargetMethods() + { + yield return typeof(Dialog_BeginRitual).GetConstructors(BindingFlags.Public | BindingFlags.Instance).First(); + } + + static IEnumerable Transpiler(IEnumerable insts, ILGenerator gen) + { + var list = insts.ToList(); + var brLabel = gen.DefineLabel(); + + foreach (var inst in list) + { + if (inst.operand == listClear) + { + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(DontClearDialogBeginRitualCache), nameof(ShouldCancelCacheClear))); + yield return new CodeInstruction(OpCodes.Brfalse, brLabel); + yield return new CodeInstruction(OpCodes.Pop); + yield return new CodeInstruction(OpCodes.Ret); + yield return new CodeInstruction(OpCodes.Nop) { labels = { brLabel } }; + } + + yield return inst; + } + } + + static bool ShouldCancelCacheClear() + { + return Multiplayer.Ticking || Multiplayer.ExecutingCmds; + } + } + // We do not sync Ability.QueueCastingJob - instead syncing the delegate method queueing the job. // We do this to avoid confirmation dialogs from some of the abilities, like neuroquake and a few ones added in Biotech. // This causes an issue with abilities like throne speech or leader speech - the ritual dialog itself is treated as the confirmation dialog. diff --git a/Source/Client/Syncing/ApiSerialization.cs b/Source/Client/Syncing/ApiSerialization.cs index 630cba16..e5037a2d 100644 --- a/Source/Client/Syncing/ApiSerialization.cs +++ b/Source/Client/Syncing/ApiSerialization.cs @@ -11,7 +11,7 @@ public static class ApiSerialization public static void Init() { - syncSimples = TypeCache.AllImplementationsOrdered(typeof(ISyncSimple)); - sessions = TypeCache.AllImplementationsOrdered(typeof(Session)); + syncSimples = TypeCache.AllInterfaceImplementationsOrdered(typeof(ISyncSimple)); + sessions = TypeCache.AllSubclassesNonAbstractOrdered(typeof(Session)); } } diff --git a/Source/Client/Syncing/Dict/SyncDictDlc.cs b/Source/Client/Syncing/Dict/SyncDictDlc.cs index 53fbb638..2f0e2eae 100644 --- a/Source/Client/Syncing/Dict/SyncDictDlc.cs +++ b/Source/Client/Syncing/Dict/SyncDictDlc.cs @@ -188,7 +188,7 @@ public static class SyncDictDlc } }, { - // Currently only used for PawnRitualRoleSelectionWidget syncing + // Currently only used for Dialog_BeginRitual delegate syncing (ByteWriter data, PawnRitualRoleSelectionWidget dialog) => { WriteSync(data, dialog.ritualAssignments); WriteSync(data, dialog.ritual); @@ -201,6 +201,20 @@ public static class SyncDictDlc return new PawnRitualRoleSelectionWidget(assignments, ritual, assignments.session.data.target, assignments.session.data.outcome); } }, + { + // Currently only used for Dialog_BeginRitual delegate syncing + (ByteWriter data, Dialog_BeginRitual dialog) => { + WriteSync(data, dialog.assignments); + WriteSync(data, dialog.ritual); + }, + (ByteReader data) => { + var assignment = ReadSync(data) as MpRitualAssignments; + if (assignment == null) return null; + + var ritual = ReadSync(data); // todo handle ritual becoming null? + return new Dialog_BeginRitual(assignment, ritual, assignment.ritualTarget, assignment.session.data.outcome); + } + }, { // This dialog has nothing of interest to us besides the methods which we need for syncing (ByteWriter _, Dialog_StyleSelection _) => { }, diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 5161ee6e..b9d5e4ba 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -63,7 +63,7 @@ public static void Init() SyncDelegate.Lambda(typeof(CaravanVisitUtility), nameof(CaravanVisitUtility.TradeCommand), 0).CancelIfAnyFieldNull(); // Caravan trade with settlement SyncDelegate.Lambda(typeof(FactionGiftUtility), nameof(FactionGiftUtility.OfferGiftsCommand), 0).CancelIfAnyFieldNull(); // Caravan offer gifts - SyncDelegate.Lambda(typeof(Building_Bed), nameof(Building_Bed.GetFloatMenuOptions), 0).CancelIfAnyFieldNull(); // Use medical bed + SyncDelegate.Lambda(typeof(Building_Bed), nameof(Building_Bed.GetBedRestFloatMenuOption), 0).CancelIfAnyFieldNull(); // Use medical bed SyncMethod.Lambda(typeof(CompRefuelable), nameof(CompRefuelable.CompGetGizmosExtra), 1); // Toggle Auto-refuel SyncMethod.Lambda(typeof(CompRefuelable), nameof(CompRefuelable.CompGetGizmosExtra), 2).SetDebugOnly(); // Set fuel to 0 @@ -284,7 +284,7 @@ The UI's main interaction area is split into three types of groups of pawns. var ritualRolesSerializer = Serializer.New( (IEnumerable roles, object target, object[] _) => { - var roleSelectionWidget = (PawnRoleSelectionWidgetBase)target; + 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) diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index 65cdd98c..5a7d3181 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -4,9 +4,9 @@ namespace Multiplayer.Client; -public static class ImplSerialization +internal static class ImplSerialization { - public static void Init() + public static void Init(RwSyncTypeHelper typeHelper) { Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IStoreSettingsParent)); Multiplayer.serialization.RegisterForSyncWithImpl(typeof(IStorageGroupMember)); @@ -21,6 +21,7 @@ public static void Init() Multiplayer.serialization.RegisterForSyncWithImpl(typeof(Policy)); // todo for 1.5 - // Multiplayer.serialization.RegisterForSyncWithImpl(typeof(PawnRoleSelectionWidgetBase)); + 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 f9dfcae7..56f54899 100644 --- a/Source/Client/Syncing/RwSerialization.cs +++ b/Source/Client/Syncing/RwSerialization.cs @@ -16,18 +16,24 @@ public static class RwSerialization { public static void Init() { - Multiplayer.serialization = new Common.SyncSerialization(new RwSyncTypeHelper()); + var typeHelper = new RwSyncTypeHelper(); + Multiplayer.serialization = new Common.SyncSerialization(typeHelper); // CanHandle hooks Multiplayer.serialization.AddCanHandleHook(syncType => { var type = syncType.type; + + // Verse.Pair<,> if (type.IsGenericType && type.GetGenericTypeDefinition() is { } gtd) if (gtd == typeof(Pair<,>)) return Multiplayer.serialization.CanHandleGenericArgs(type); + // Verse.IExposable if (syncType.expose) return typeof(IExposable).IsAssignableFrom(type); + + // Multiplayer.API.ISyncSimple if (type == typeof(ISyncSimple)) return true; if (typeof(ISyncSimple).IsAssignableFrom(type)) @@ -35,12 +41,8 @@ public static void Init() Where(t => type.IsAssignableFrom(t)). SelectMany(AccessTools.GetDeclaredFields). All(f => Multiplayer.serialization.CanHandle(f.FieldType)); - if (typeof(Def).IsAssignableFrom(type)) - return true; - if (typeof(Designator).IsAssignableFrom(type)) - return true; - return SyncDict.syncWorkers.TryGetValue(type, out _); + return false; }); // Verse.Pair<,> serialization @@ -66,7 +68,7 @@ public static void Init() } ); - // IExposable serialization + // Verse.IExposable serialization Multiplayer.serialization.AddSerializationHook( syncType => syncType.expose, (data, syncType) => @@ -90,7 +92,7 @@ public static void Init() } ); - // ISyncSimple serialization + // Multiplayer.API.ISyncSimple serialization // todo null handling for ISyncSimple? Multiplayer.serialization.AddSerializationHook( syncType => typeof(ISyncSimple).IsAssignableFrom(syncType.type), @@ -111,7 +113,7 @@ public static void Init() } ); - ImplSerialization.Init(); + ImplSerialization.Init(typeHelper); CompSerialization.Init(); ApiSerialization.Init(); DefSerialization.Init(); diff --git a/Source/Client/Syncing/RwSyncTypeHelper.cs b/Source/Client/Syncing/RwSyncTypeHelper.cs index a245fd58..dd1c2b0e 100644 --- a/Source/Client/Syncing/RwSyncTypeHelper.cs +++ b/Source/Client/Syncing/RwSyncTypeHelper.cs @@ -7,8 +7,18 @@ 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/Util/TypeCache.cs b/Source/Client/Util/TypeCache.cs index bdc868e5..d0393044 100644 --- a/Source/Client/Util/TypeCache.cs +++ b/Source/Client/Util/TypeCache.cs @@ -63,7 +63,7 @@ public static IEnumerable AllImplementing(this Type type) return interfaceImplementations.GetValueSafe(type) ?? Enumerable.Empty(); } - public static Type[] AllImplementationsOrdered(Type type) + public static Type[] AllInterfaceImplementationsOrdered(Type type) { return type.AllImplementing() .OrderBy(t => t.IsInterface) diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index 0d6055ef..a17b9188 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -150,7 +150,7 @@ Anomaly compatibility is also a work-in-progress. Compatibility with other mods We recommend downgrading RimWorld to 1.4 if you want to continue playing a stable version. """; - TooltipHandler.TipRegion(new Rect(x, 0, 400, 300), v15Notice); + TooltipHandler.TipRegion(new Rect(x, 0, 400, 25), v15Notice); if (false) // todo Button( diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index f31f47b8..039415ad 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -12,7 +12,7 @@ - + diff --git a/Source/Common/Syncing/SyncSerialization.cs b/Source/Common/Syncing/SyncSerialization.cs index e6aaa34c..6dae3d3e 100644 --- a/Source/Common/Syncing/SyncSerialization.cs +++ b/Source/Common/Syncing/SyncSerialization.cs @@ -98,7 +98,6 @@ public bool CanHandleGenericArgs(Type genericType) // For each serializationHooks: // if matcher: serialize; break // Implementations of Multiplayer.API.ISynchronizable - // Run all typeChangerHooks // syncTree.TryGetValue(type) public object? ReadSyncObject(ByteReader data, SyncType syncType) @@ -311,9 +310,6 @@ public void WriteSyncObject(ByteWriter data, object? obj, SyncType syncType) var log = (data as LoggingByteWriter)?.Log; log?.Enter($"{type.FullName}: {obj ?? "null"}"); - if (obj != null && !type.IsInstanceOfType(obj)) - throw new SerializationException($"Serializing with type {type} but got object of type {obj.GetType()}"); - try { if (typeof(object) == type) diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 231d9ccf..db91a7f4 100644 --- a/Source/Common/Version.cs +++ b/Source/Common/Version.cs @@ -2,8 +2,8 @@ namespace Multiplayer.Common { public static class MpVersion { - public const string Version = "0.10.2"; - public const int Protocol = 44; + public const string Version = "0.10.3"; + public const int Protocol = 45; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index fea717ee..195aae26 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive