diff --git a/About/ModIcon.png b/About/ModIcon.png new file mode 100644 index 00000000..a2d0ad41 Binary files /dev/null and b/About/ModIcon.png differ diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index a38b0088..0c443666 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/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index df943b0b..34ce2342 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -353,7 +353,8 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po var canEverSpectate = typeof(RitualRoleAssignments).GetMethod(nameof(RitualRoleAssignments.CanEverSpectate)); var effectMethods = new MethodBase[] { subSustainerStart, sampleCtor, subSoundPlay, effecterTick, effecterTrigger, effecterCleanup, randomBoltMesh, drawTrackerCtor, randomHair }; - var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public); + var moteMethods = typeof(MoteMaker).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Concat(typeof(CompCableConnection.Cable).GetMethod(nameof(CompCableConnection.Cable.Tick))); var fleckMethods = typeof(FleckMaker).GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(m => m.ReturnType == typeof(void)) .Concat(typeof(FleckManager).GetMethods() // FleckStatic uses Rand in Setup method, FleckThrown uses RandomInRange in TimeInterval. May as well catch all in case mods do the same. diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 90c81df5..8acb9feb 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -15,16 +15,19 @@ namespace Multiplayer.Client.Patches { + [EarlyPatch] [HarmonyPatch(typeof(PawnTweener))] [HarmonyPatch(nameof(PawnTweener.TweenedPos), MethodType.Getter)] static class DrawPosPatch { - static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface; + public static bool returnTruePosition = false; + + static bool Prefix() => Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition; // Give the root position during ticking static void Postfix(PawnTweener __instance, ref Vector3 __result) { - if (Multiplayer.Client == null || Multiplayer.InInterface) return; + if (Multiplayer.Client == null || Multiplayer.InInterface || returnTruePosition) return; __result = __instance.TweenedPosRoot(); } } @@ -524,6 +527,66 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(UndercaveMapComponent), nameof(UndercaveMapComponent.MapComponentTick))] + static class DeterministicUndercaveRockCollapse + { + static IEnumerable Transpiler(IEnumerable instr) + { + var target = MethodOf.Lambda(Rand.MTBEventOccurs); + + foreach (var ci in instr) + { + yield return ci; + + // Add "& false" to any call to Rand.MTBEventOccurs. + // We'll handle those calls in our postfix. + if (ci.Calls(target)) + { + yield return new CodeInstruction(OpCodes.Ldc_I4_0); + yield return new CodeInstruction(OpCodes.And); + } + } + } + + static void Prefix() => Rand.PushState(); + + static void Postfix(UndercaveMapComponent __instance) + { + // Pop the RNG state from the prefix + Rand.PopState(); + + // Make sure the pit gate is collapsing + if (__instance.pitGate is not { IsCollapsing: true }) + return; + + // Check if the rocks should collapse + var mtb = UndercaveMapComponent.HoursToShakeMTBTicksCurve.Evaluate(__instance.pitGate.TicksUntilCollapse / 2500f); + if (!Rand.MTBEventOccurs(mtb, 1, 1)) + return; + + // Since the number of RNG calls will depend on numDustEffecters argument, we need to push/pop the RNG state. + // The RNG calls related to simulation will happen first, followed by the one determined by amount of + // effecters - it would not be MP safe, but since it happens last it will be fine once we pop the state. + Rand.PushState(); + + // If not looking at the map, trigger the collapse without shake/effecters (since it's not needed for current player). + // The call to play a sound is handled by RW itself, since it targets a specific map already. + if (Find.CurrentMap != __instance.map) + { + // Progress the RNG state, matching the RandomInRange call in other two cases + Rand.RangeInclusive(0, 100); + __instance.TriggerCollapseFX(0, 0); + } + // Else, follow vanilla shake/effecter rules + else if (__instance.pitGate.CollapseStage == 1) + __instance.TriggerCollapseFX(UndercaveMapComponent.StageOneShakeAmount, UndercaveMapComponent.StageOneNumCollapseEffects.RandomInRange); + else + __instance.TriggerCollapseFX(UndercaveMapComponent.StageTwoShakeAmount, UndercaveMapComponent.StageTwoNumCollapseEffects.RandomInRange); + + Rand.PopState(); + } + } + [HarmonyPatch(typeof(CompObelisk_Abductor), nameof(CompObelisk_Abductor.GenerateLabyrinth))] static class SynchronousAbductorObelisk { diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index a0c35a87..37e4e25c 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -9,6 +9,7 @@ using System.Reflection.Emit; using System.Text.RegularExpressions; using System.Xml.Linq; +using Multiplayer.Client.Patches; using UnityEngine; using Verse; using Verse.AI; @@ -581,4 +582,12 @@ static void FixStorage(IStoreSettingsParent __instance, StorageSettings ___allow ___allowedNutritionSettings.owner ??= __instance; } } + + [HarmonyPatch(typeof(MoteAttachLink), nameof(MoteAttachLink.UpdateDrawPos))] + static class MoteAttachLinkUsesTruePosition + { + static void Prefix() => DrawPosPatch.returnTruePosition = true; + + static void Finalizer() => DrawPosPatch.returnTruePosition = false; + } } diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index d4301500..cef7d7d3 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -98,11 +98,12 @@ static void Prefix(ref Action action) { if (Multiplayer.Client != null && (Multiplayer.Ticking || Multiplayer.ExecutingCmds)) { - action = PushState + action + Rand.PopState; + var seed = Rand.Int; + action = (() => PushState(seed)) + action + Rand.PopState; } } - static void PushState() => Rand.PushState(4); + static void PushState(int seed) => Rand.PushState(seed); } // Seed the rotation random diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index c2591c1d..cc96246e 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -13,16 +13,19 @@ public static class UniqueIdsPatch // Start at -2 because -1 is sometimes used as the uninitialized marker private static int localIds = -2; + public static bool useLocalIdsOverride; + + private static bool UseLocalIds => + Multiplayer.Client != null && (useLocalIdsOverride || Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry); + static bool Prefix() { - return Multiplayer.Client == null || (!Multiplayer.InInterface && Current.ProgramState != ProgramState.Entry); + return !UseLocalIds; } static void Postfix(ref int __result) { - if (Multiplayer.Client == null) return; - - if (Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry) + if (UseLocalIds) __result = localIds--; } } diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index 18922d96..9dab7444 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -2,6 +2,8 @@ using RimWorld; using RimWorld.Planet; using System; +using Multiplayer.API; +using Multiplayer.Client.Patches; using UnityEngine; using Verse; using static Verse.Widgets; @@ -147,7 +149,7 @@ static class CancelDialogFormCaravan { static bool Prefix(Window window) { - if (Multiplayer.MapContext != null && window.GetType() == typeof(Dialog_FormCaravan)) + if (Multiplayer.Client != null && window.GetType() == typeof(Dialog_FormCaravan)) return false; return true; @@ -155,21 +157,76 @@ static bool Prefix(Window window) } [HarmonyPatch(typeof(Dialog_FormCaravan), MethodType.Constructor)] - [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3) })] + [HarmonyPatch(new[] { typeof(Map), typeof(bool), typeof(Action), typeof(bool), typeof(IntVec3?) })] static class DialogFormCaravanCtorPatch { - static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved) + static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? designatedMeetingPoint) { + if (Multiplayer.Client == null) + return; + if (__instance.GetType() != typeof(Dialog_FormCaravan)) return; // Handles showing the dialog from TimedForcedExit.CompTick -> TimedForcedExit.ForceReform + // (note TimedForcedExit is obsolete) if (Multiplayer.ExecutingCmds || Multiplayer.Ticking) { var comp = map.MpComp(); if (comp.sessionManager.GetFirstOfType() == null) - comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved); + comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved, designatedMeetingPoint); } + else // Handles opening from the interface: forming gizmos, reforming gizmos and caravan hitching spots + { + StartFormingCaravan(map, reform, designatedMeetingPoint); + } + } + + [SyncMethod] + internal static void StartFormingCaravan(Map map, bool reform = false, IntVec3? designatedMeetingPoint = null, int? routePlannerWaypoint = null) + { + var comp = map.MpComp(); + var session = comp.CreateCaravanFormingSession(reform, null, false, designatedMeetingPoint); + + if (TickPatch.currentExecutingCmdIssuedBySelf) + { + var dialog = session.OpenWindow(); + if (routePlannerWaypoint is { } tile) + { + try + { + UniqueIdsPatch.useLocalIdsOverride = true; + + // Just to be safe + // RNG shouldn't be invoked but TryAddWaypoint is quite complex and calls pathfinding + Rand.PushState(); + + var worldRoutePlanner = Find.WorldRoutePlanner; + worldRoutePlanner.Start(dialog); + worldRoutePlanner.TryAddWaypoint(tile); + } + finally + { + Rand.PopState(); + UniqueIdsPatch.useLocalIdsOverride = false; + } + } + } + } + } + + [HarmonyPatch(typeof(FormCaravanGizmoUtility), nameof(FormCaravanGizmoUtility.DialogFromToSettlement))] + static class HandleFormCaravanShowRoutePlanner + { + static bool Prefix(Map origin, int tile) + { + if (Multiplayer.Client == null) + return true; + + // Override behavior in multiplayer + DialogFormCaravanCtorPatch.StartFormingCaravan(origin, routePlannerWaypoint: tile); + + return false; } } diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs index c8886833..8536b51b 100644 --- a/Source/Client/Persistent/CaravanFormingProxy.cs +++ b/Source/Client/Persistent/CaravanFormingProxy.cs @@ -11,8 +11,11 @@ public class CaravanFormingProxy : Dialog_FormCaravan, ISwitchToMap public CaravanFormingSession Session => map.MpComp().sessionManager.GetFirstOfType(); - public CaravanFormingProxy(Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot) + public int originalSessionId; + + public CaravanFormingProxy(int originalSessionId, Map map, bool reform = false, Action onClosed = null, bool mapAboutToBeRemoved = false, IntVec3? meetingSpot = null) : base(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot) { + this.originalSessionId = originalSessionId; } public override void DoWindowContents(Rect inRect) diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 7f2639af..fd1fc32a 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -44,7 +44,7 @@ public CaravanFormingSession(Map map, bool reform, Action onClosed, bool mapAbou private void AddItems() { - var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot) + var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot) { autoSelectTravelSupplies = autoSelectTravelSupplies }; @@ -52,7 +52,7 @@ private void AddItems() transferables = dialog.transferables; } - public void OpenWindow(bool sound = true) + public CaravanFormingProxy OpenWindow(bool sound = true) { var dialog = PrepareDummyDialog(); if (!sound) @@ -72,13 +72,14 @@ public void OpenWindow(bool sound = true) ); dialog.Notify_TransferablesChanged(); - Find.WindowStack.Add(dialog); + + return dialog; } private CaravanFormingProxy PrepareDummyDialog() { - var dialog = new CaravanFormingProxy(map, reform, null, mapAboutToBeRemoved, meetingSpot) + var dialog = new CaravanFormingProxy(sessionId, map, reform, null, mapAboutToBeRemoved, meetingSpot) { transferables = transferables, startingTile = startingTile, @@ -142,7 +143,9 @@ public void Cancel() private void Remove() { map.MpComp().sessionManager.RemoveSession(this); - Find.WorldRoutePlanner.Stop(); + + if (Find.WorldRoutePlanner.currentFormCaravanDialog is CaravanFormingProxy proxy && proxy.originalSessionId == sessionId) + Find.WorldRoutePlanner.Stop(); } [SyncMethod] @@ -186,7 +189,6 @@ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry { return new FloatMenuOption("MpCaravanFormingSession".Translate(), () => { - SwitchToMapOrWorld(entry.map); OpenWindow(); }); } diff --git a/Source/Client/Persistent/RitualBeginProxy.cs b/Source/Client/Persistent/RitualBeginProxy.cs new file mode 100644 index 00000000..7f3b015f --- /dev/null +++ b/Source/Client/Persistent/RitualBeginProxy.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Persistent; + +public class RitualBeginProxy : Dialog_BeginRitual, ISwitchToMap +{ + public static RitualBeginProxy drawing; + + public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); + + // 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 RitualBeginProxy( + 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 }) + { + cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole)); + Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole); + if (preceptRole != null) + cachedRoles.Add(preceptRole); + cachedRoles.SortBy(x => x.def.displayOrderInImpact); + } + } + + public override void DoWindowContents(Rect inRect) + { + drawing = this; + + try + { + var session = Session; + + if (session == null) + { + soundClose = SoundDefOf.Click; + Close(); + } + + // Make space for the "Switch to map" button + inRect.yMin += 20f; + + base.DoWindowContents(inRect); + } + finally + { + drawing = null; + } + } +} diff --git a/Source/Client/Persistent/RitualData.cs b/Source/Client/Persistent/RitualData.cs index 2827e942..9028a8b0 100644 --- a/Source/Client/Persistent/RitualData.cs +++ b/Source/Client/Persistent/RitualData.cs @@ -6,6 +6,11 @@ namespace Multiplayer.Client.Persistent { + public class MpRitualAssignments : RitualRoleAssignments + { + public RitualSession session; + } + public class RitualData : ISynchronizable { public Precept_Ritual ritual; diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/RitualPatches.cs similarity index 56% rename from Source/Client/Persistent/Rituals.cs rename to Source/Client/Persistent/RitualPatches.cs index c5ebcc91..e8cff093 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/RitualPatches.cs @@ -11,179 +11,12 @@ namespace Multiplayer.Client.Persistent { - public class RitualSession : SemiPersistentSession - { - public Map map; - public RitualData data; - - public override Map Map => map; - - public RitualSession(Map map) : base(map) - { - this.map = map; - } - - public RitualSession(Map map, RitualData data) : this(map) - { - this.data = data; - this.data.assignments.session = this; - } - - [SyncMethod] - public void Remove() - { - map.MpComp().sessionManager.RemoveSession(this); - } - - [SyncMethod] - public void Start() - { - if (data.action != null && data.action(data.assignments)) - Remove(); - } - - public void OpenWindow(bool sound = true) - { - var dialog = new BeginRitualProxy( - data.assignments, - data.ritualLabel, - data.ritual, - data.target, - map, - data.action, - data.organizer, - data.obligation, - null, - data.confirmText, - null, - null, - data.outcome, - data.extraInfos, - null - ); - - if (!sound) - dialog.soundAppear = null; - - Find.WindowStack.Add(dialog); - } - - public override void Sync(SyncWorker sync) - { - if (sync.isWriting) - { - sync.Write(data); - } - else - { - data = sync.Read(); - data.assignments.session = this; - } - } - - public override bool IsCurrentlyPausing(Map map) => map == this.map; - - public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) - { - return new FloatMenuOption("MpRitualSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - OpenWindow(); - }); - } - } - - public class MpRitualAssignments : RitualRoleAssignments - { - public RitualSession session; - } - - public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap - { - public static BeginRitualProxy drawing; - - public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); - - // 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 }) - { - cachedRoles.AddRange(ritual.ideo.RolesListForReading.Where(r => !r.def.leaderRole)); - Precept_Role preceptRole = Faction.OfPlayer.ideos.PrimaryIdeo.RolesListForReading.FirstOrDefault(p => p.def.leaderRole); - if (preceptRole != null) - cachedRoles.Add(preceptRole); - cachedRoles.SortBy(x => x.def.displayOrderInImpact); - } - } - - public override void DoWindowContents(Rect inRect) - { - drawing = this; - - try - { - var session = Session; - - if (session == null) - { - soundClose = SoundDefOf.Click; - Close(); - } - - // Make space for the "Switch to map" button - inRect.yMin += 20f; - - base.DoWindowContents(inRect); - } - finally - { - drawing = null; - } - } - } - [HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))] static class MakeCancelRitualButtonRed { static void Prefix(string label, ref bool __state) { - if (BeginRitualProxy.drawing == null) return; + if (RitualBeginProxy.drawing == null) return; if (label != "CancelButton".Translate()) return; GUI.color = new Color(1f, 0.3f, 0.35f); @@ -197,7 +30,7 @@ static void Postfix(bool __state, ref DraggableResult __result) GUI.color = Color.white; if (__result.AnyPressed()) { - BeginRitualProxy.drawing.Session?.Remove(); + RitualBeginProxy.drawing.Session?.Remove(); __result = DraggableResult.Idle; } } @@ -208,7 +41,7 @@ static class HandleStartRitual { static bool Prefix(Dialog_BeginRitual __instance) { - if (__instance is BeginRitualProxy proxy) + if (__instance is RitualBeginProxy proxy) { proxy.Session.Start(); return false; @@ -224,7 +57,7 @@ static class CancelDialogBeginRitual static bool Prefix(Window window) { if (Multiplayer.Client != null - && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let BeginRitualProxy through + && window.GetType() == typeof(Dialog_BeginRitual) // Doesn't let RitualBeginProxy through && (Multiplayer.ExecutingCmds || Multiplayer.Ticking)) { var tempDialog = (Dialog_BeginRitual)window; diff --git a/Source/Client/Persistent/RitualSession.cs b/Source/Client/Persistent/RitualSession.cs new file mode 100644 index 00000000..6010eed0 --- /dev/null +++ b/Source/Client/Persistent/RitualSession.cs @@ -0,0 +1,87 @@ +using Multiplayer.API; +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Persistent; + +public class RitualSession : SemiPersistentSession +{ + public Map map; + public RitualData data; + + public override Map Map => map; + + public RitualSession(Map map) : base(map) + { + this.map = map; + } + + public RitualSession(Map map, RitualData data) : this(map) + { + this.data = data; + this.data.assignments.session = this; + } + + [SyncMethod] + public void Remove() + { + map.MpComp().sessionManager.RemoveSession(this); + } + + [SyncMethod] + public void Start() + { + if (data.action != null && data.action(data.assignments)) + Remove(); + } + + public void OpenWindow(bool sound = true) + { + var dialog = new RitualBeginProxy( + data.assignments, + data.ritualLabel, + data.ritual, + data.target, + map, + data.action, + data.organizer, + data.obligation, + null, + data.confirmText, + null, + null, + data.outcome, + data.extraInfos, + null + ); + + if (!sound) + dialog.soundAppear = null; + + Find.WindowStack.Add(dialog); + } + + public override void Sync(SyncWorker sync) + { + if (sync.isWriting) + { + sync.Write(data); + } + else + { + data = sync.Read(); + data.assignments.session = this; + } + } + + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpRitualSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } +} diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index b145fc5b..caf07fa3 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -320,6 +320,13 @@ private static void InitChoiceLetters() // Growth moment for a child CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice(AccessTools.Method(typeof(SyncDelegates), nameof(PickRandomTraitAndPassions)), typeof(ChoiceLetter_GrowthMoment)); SyncMethod.Register(typeof(ChoiceLetter_GrowthMoment), nameof(ChoiceLetter_GrowthMoment.MakeChoices)).ExposeParameter(1); + + // Creep joiner + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 0); // Accept joiner + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 1); // Arrest joiner + CloseDialogsForExpiredLetters.RegisterDefaultLetterChoice( + SyncMethod.LambdaInGetter(typeof(ChoiceLetter_AcceptCreepJoiner), nameof(ChoiceLetter_AcceptCreepJoiner.Choices), 2) + .method); // Reject joiner } static void SyncBabyToChildLetter(ChoiceLetter_BabyToChild letter) @@ -364,7 +371,7 @@ static void SetBabyName(ChoiceLetter_BabyBirth letter) } // If the baby ended up being stillborn, the timer to name them is 1 tick. This patch is here to allow players in MP to actually change their name. - [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome))] + [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome_NewTemp))] static void GiveTimeToNameStillborn(Thing __result) { if (Multiplayer.Client != null && __result is Pawn pawn && pawn.health.hediffSet.HasHediff(HediffDefOf.Stillborn)) @@ -468,54 +475,6 @@ static void GeneUIUtilityTarget(Thing target) geneUIUtilityTarget = target; } - [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 0)] - static bool GizmoFormCaravan(MapParent ___mapParent) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(___mapParent.Map, false); - return false; - } - - [MpPrefix(typeof(FormCaravanComp), nameof(FormCaravanComp.GetGizmos), lambdaOrdinal: 1)] - static bool GizmoReformCaravan(MapParent ___mapParent) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(___mapParent.Map, true); - return false; - } - - [MpPrefix(typeof(CompHitchingSpot), nameof(CompHitchingSpot.CompGetGizmosExtra), 0)] - static bool GizmoFormCaravan(CompHitchingSpot __instance) - { - if (Multiplayer.Client == null) return true; - GizmoFormCaravan(__instance.parent.Map, false, __instance.parent.Position); - return false; - } - - private static void GizmoFormCaravan(Map map, bool reform, IntVec3? meetingSpot = null) - { - var comp = map.MpComp(); - - if (comp.sessionManager.GetFirstOfType() is { } session) - session.OpenWindow(); - else - CreateCaravanFormingSession(comp, reform, meetingSpot); - } - - [SyncMethod] - private static void CreateCaravanFormingSession(MultiplayerMapComp comp, bool reform, IntVec3? meetingSpot = null) - { - var session = comp.CreateCaravanFormingSession(reform, null, false, meetingSpot); - - if (TickPatch.currentExecutingCmdIssuedBySelf) - { - session.OpenWindow(); - AsyncTimeComp.keepTheMap = true; - Current.Game.CurrentMap = comp.map; - Find.World.renderer.wantedMode = WorldRenderMode.None; - } - } - [MpPostfix(typeof(CaravanVisitUtility), nameof(CaravanVisitUtility.TradeCommand))] static void ReopenTradingWindowLocally(Caravan caravan, Command __result) { diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index d2351e09..dc63361b 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -105,7 +105,7 @@ public static void Init() SyncMethod.Register(typeof(Building_SunLamp), nameof(Building_SunLamp.MakeMatchingGrowZone)); SyncMethod.Register(typeof(Building_ShipComputerCore), nameof(Building_ShipComputerCore.TryLaunch)); SyncMethod.Register(typeof(CompPower), nameof(CompPower.TryManualReconnect)); - SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature)); + SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature_NewTemp)); SyncMethod.Register(typeof(CompTransporter), nameof(CompTransporter.CancelLoad), Array.Empty()); SyncMethod.Register(typeof(MapPortal), nameof(MapPortal.CancelLoad)); SyncMethod.Register(typeof(StorageSettings), nameof(StorageSettings.CopyFrom)).ExposeParameter(0); diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 039415ad..7f39a9d6 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -12,7 +12,7 @@ - + diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index db91a7f4..b43cf4a9 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.3"; - public const int Protocol = 45; + public const string Version = "0.10.4"; + public const int Protocol = 46; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index 195aae26..5572bda9 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs index b927f4bd..759b4cb3 100644 --- a/Source/MultiplayerLoader/SyncRituals.cs +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -47,9 +47,9 @@ void Register(Type baseType, string method, Type derivedType, Action