diff --git a/Source/Client/Persistent/PsychicRitualBeginProxy.cs b/Source/Client/Persistent/PsychicRitualBeginProxy.cs new file mode 100644 index 00000000..43f12da4 --- /dev/null +++ b/Source/Client/Persistent/PsychicRitualBeginProxy.cs @@ -0,0 +1,77 @@ +using RimWorld; +using UnityEngine; +using Verse; +using Verse.AI.Group; + +namespace Multiplayer.Client.Persistent; + +public class PsychicRitualBeginProxy : Dialog_BeginPsychicRitual, ISwitchToMap +{ + public static PsychicRitualBeginProxy drawing; + + public PsychicRitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); + + public PsychicRitualBeginProxy( + PsychicRitualDef def, + PsychicRitualCandidatePool candidatePool, + PsychicRitualRoleAssignments assignments, + Map map) : + base(def, candidatePool, assignments, map) + { + var session = Session; + + if (Session == null) + { + Log.Error("Trying to open a psychic ritual dialog proxy without session active"); + return; + } + + try + { + // Ensure that InitializeCast call is seeded, use session and map IDs to get a somewhat random value. + // We could also include current map tick as well, if needed. + Rand.PushState(Gen.HashCombineInt(session.SessionId, map.uniqueID)); + + // Recache the pending issues for the ritual. + // Each time the ritual is started, this method is called. However, + // in MP we can have multiple rituals active at a time, so ensure + // that we recache if the ritual is valid on a specific map. + // If there's ever issues with this, we may need to call this + // in DoWindowContents, however it shouldn't be needed. + def.InitializeCast(map); + } + finally + { + // Pop RNG state in finally to ensure no issues when an exception occurs. + Rand.PopState(); + } + } + + public override void DoWindowContents(Rect inRect) + { + drawing = this; + + try + { + var session = Session; + + if (session == null) + { + soundClose = SoundDefOf.Click; + Close(); + } + + base.DoWindowContents(inRect); + } + finally + { + drawing = null; + } + } + + public override void Start() + { + if (CanBegin) + Session?.Start(); + } +} diff --git a/Source/Client/Persistent/PsychicRitualPatches.cs b/Source/Client/Persistent/PsychicRitualPatches.cs new file mode 100644 index 00000000..dcc4596d --- /dev/null +++ b/Source/Client/Persistent/PsychicRitualPatches.cs @@ -0,0 +1,45 @@ +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.AI.Group; + +namespace Multiplayer.Client.Persistent; + +[HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))] +static class MakeCancelPsychicRitualButtonRed +{ + static void Prefix(string label, ref bool __state) + { + if (PsychicRitualBeginProxy.drawing == null) return; + if (label != "CancelButton".Translate()) return; + + GUI.color = new Color(1f, 0.3f, 0.35f); + __state = true; + } + + static void Postfix(bool __state, ref Widgets.DraggableResult __result) + { + if (!__state) return; + + GUI.color = Color.white; + if (__result.AnyPressed()) + { + PsychicRitualBeginProxy.drawing.Session?.Remove(); + __result = Widgets.DraggableResult.Idle; + } + } +} + +[HarmonyPatch(typeof(PsychicRitualGizmo), nameof(PsychicRitualGizmo.InitializePsychicRitual))] +static class CancelDialogBeginPsychicRitual +{ + static bool Prefix(PsychicRitualDef_InvocationCircle psychicRitualDef, Thing target) + { + if (Multiplayer.Client == null) + return true; + + PsychicRitualSession.OpenOrCreateSession(psychicRitualDef, target); + return false; + } +} diff --git a/Source/Client/Persistent/PsychicRitualSession.cs b/Source/Client/Persistent/PsychicRitualSession.cs new file mode 100644 index 00000000..edb5257c --- /dev/null +++ b/Source/Client/Persistent/PsychicRitualSession.cs @@ -0,0 +1,120 @@ +using Multiplayer.API; +using RimWorld; +using Verse; +using Verse.AI.Group; + +namespace Multiplayer.Client.Persistent; + +public class PsychicRitualSession : SemiPersistentSession, ISessionWithCreationRestrictions +{ + public Map map; + public PsychicRitualDef ritual; + public PsychicRitualCandidatePool candidatePool; + public MpPsychicRitualAssignments assignments; + + public override Map Map => map; + + public PsychicRitualSession(Map map) : base(map) + { + this.map = map; + } + + public PsychicRitualSession(Map map, PsychicRitualDef ritual, PsychicRitualCandidatePool candidatePool, MpPsychicRitualAssignments assignments) : this(map) + { + this.ritual = ritual; + this.assignments = assignments; + this.candidatePool = candidatePool; + this.assignments.session = this; + } + + public static void OpenOrCreateSession(PsychicRitualDef_InvocationCircle ritual, Thing target) + { + // We need Find.CurrentMap to match the map we're creating the session in + var map = Find.CurrentMap; + if (map != target.Map) + { + Log.Error($"Error opening/creating {nameof(PsychicRitualSession)} - current map ({Find.CurrentMap}) does not match ritual spot map ({target.Map})."); + return; + } + + var session = map.MpComp().sessionManager.GetFirstOfType(); + if (session == null) + CreateSession(ritual, target); + else + session.OpenWindow(); + } + + // Need CurrentMap for PsychicRitualDef.FindCandidatePool call + [SyncMethod(SyncContext.CurrentMap)] + public static void CreateSession(PsychicRitualDef_InvocationCircle ritual, Thing target) + { + var map = Find.CurrentMap; + + // Get role assignments and candidate pool + var candidatePool = ritual.FindCandidatePool(); + var assignments = MpUtil.ShallowCopy(ritual.BuildRoleAssignments(target), new MpPsychicRitualAssignments()); + + var manager = map.MpComp().sessionManager; + var session = manager.GetOrAddSession(new PsychicRitualSession(map, ritual, candidatePool, assignments)); + + if (TickPatch.currentExecutingCmdIssuedBySelf) + session.OpenWindow(); + } + + [SyncMethod] + public void Remove() + { + map.MpComp().sessionManager.RemoveSession(this); + } + + [SyncMethod] + public void Start() + { + Remove(); + ritual.MakeNewLord(assignments); + Find.PsychicRitualManager.RegisterCooldown(ritual); + } + + public void OpenWindow(bool sound = true) + { + var dialog = new PsychicRitualBeginProxy( + ritual, + candidatePool, + assignments, + map); + + if (!sound) + dialog.soundAppear = null; + + Find.WindowStack.Add(dialog); + } + + public override void Sync(SyncWorker sync) + { + sync.Bind(ref ritual); + sync.Bind(ref candidatePool); + + SyncType assignmentsType = typeof(MpPsychicRitualAssignments); + assignmentsType.expose = true; + sync.Bind(ref assignments, assignmentsType); + assignments.session = this; + } + + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpPsychicRitualSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } + + public bool CanExistWith(Session other) => other is not PsychicRitualSession; +} + +public class MpPsychicRitualAssignments : PsychicRitualRoleAssignments +{ + public PsychicRitualSession session; +} diff --git a/Source/Client/Syncing/Dict/SyncDictDlc.cs b/Source/Client/Syncing/Dict/SyncDictDlc.cs index ce91639e..85397563 100644 --- a/Source/Client/Syncing/Dict/SyncDictDlc.cs +++ b/Source/Client/Syncing/Dict/SyncDictDlc.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Multiplayer.API; using Multiplayer.Client.Persistent; @@ -313,7 +314,87 @@ public static class SyncDictDlc return (ActivityGizmo)comp.gizmo; } - } + }, + { + (ByteWriter data, PsychicRitualRoleAssignments assignments) => + { + // In Multiplayer, PsychicRitualRoleAssignments should only be of the wrapper type MpRitualAssignments + var mpAssignments = (MpPsychicRitualAssignments)assignments; + data.MpContext().map = mpAssignments.session.map; + data.WriteInt32(mpAssignments.session.SessionId); + }, + (ByteReader data) => + { + var id = data.ReadInt32(); + var ritual = data.MpContext().map.MpComp().sessionManager.GetFirstWithId(id); + return ritual?.assignments; + } + }, + { + (ByteWriter data, PsychicRitualCandidatePool candidatePool) => + { + WriteSync(data, candidatePool.AllCandidatePawns); + WriteSync(data, candidatePool.NonAssignablePawns); + }, + (ByteReader data) => + { + var allCandidates = ReadSync>(data); + var nonAssignable = ReadSync>(data); + + return new PsychicRitualCandidatePool(allCandidates, nonAssignable); + } + }, + { + (ByteWriter data, PsychicRitual ritual) => + { + var lordToil = ritual?.lord?.CurLordToil as LordToil_PsychicRitual; + WriteSync(data, lordToil); + + // We could skip syncing the ID, I've decided to include it for additional error checking + if (lordToil != null) + data.WriteInt32(ritual.loadID); + }, + (ByteReader data) => + { + var lordToil = ReadSync(data); + if (lordToil == null) + return null; + + var ritualId = data.ReadInt32(); + var ritual = lordToil.RitualData.psychicRitual; + + if (ritual == null) + { + Log.Error("Psychic ritual was null after syncing"); + return null; + } + if (ritual.loadID != ritualId) + { + Log.Error($"Synced psychic ritual ID did not match after syncing, expected: {ritualId}, current: {ritual.loadID}"); + return null; + } + + return ritual; + }, true // implicit + }, + { + // Currently only used for Dialog_BeginPsychicRitual delegate syncing + (ByteWriter data, PawnPsychicRitualRoleSelectionWidget dialog) => + { + // psychicRitualAssignments and assignments fields store the same object. + // If we used assignments field we would have to cast it before syncing. + WriteSync(data, dialog.psychicRitualAssignments); + WriteSync(data, dialog.ritualDef); + }, + (ByteReader data) => + { + var assignments = (MpPsychicRitualAssignments)ReadSync(data); + if (assignments == null) return null; + + var ritual = ReadSync(data); // todo handle ritual becoming null? + return new PawnPsychicRitualRoleSelectionWidget(ritual, assignments.session.candidatePool, assignments); + } + }, #endregion }; diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index caf07fa3..d21b004b 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -9,6 +9,7 @@ using Multiplayer.Client.Patches; using MultiplayerLoader; using Verse; +using Verse.AI.Group; namespace Multiplayer.Client { @@ -271,6 +272,11 @@ private static void InitRituals() 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 + SyncMethod.Register(typeof(PsychicRitual), nameof(PsychicRitual.CancelPsychicRitual)); + SyncMethod.Register(typeof(PsychicRitual), nameof(PsychicRitual.LeavePsychicRitual)); // Make pawn leave ritual + + SyncMethod.Register(typeof(GameComponent_PsychicRitualManager), nameof(GameComponent_PsychicRitualManager.ClearAllCooldowns)).SetDebugOnly(); // Dev reset all psychic ritual cooldowns + /* PawnRoleSelectionWidgetBase @@ -287,6 +293,9 @@ The UI's main interaction area is split into three types of groups of pawns. SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.TryAssignSpectate)); SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.RemoveParticipant)); + SyncMethod.Register(typeof(PsychicRitualRoleAssignments), nameof(PsychicRitualRoleAssignments.TryAssignSpectate)); + SyncMethod.Register(typeof(PsychicRitualRoleAssignments), nameof(PsychicRitualRoleAssignments.RemoveParticipant)); + SyncRituals.ApplyPrepatches(null); } diff --git a/Source/MultiplayerLoader/SyncRituals.cs b/Source/MultiplayerLoader/SyncRituals.cs index 759b4cb3..24a1c4af 100644 --- a/Source/MultiplayerLoader/SyncRituals.cs +++ b/Source/MultiplayerLoader/SyncRituals.cs @@ -55,25 +55,33 @@ void Register(Type baseType, string method, Type derivedType, Action 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) + var ritualRolesSerializer = Serializer.New, (PawnRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader + ); + var psychicRitualRolesSerializer = Serializer.New, (PawnPsychicRitualRoleSelectionWidget roleSelectionWidget, string Key)>( + RitualRoleWriter, + RitualRoleReader ); Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), typeof(PawnRitualRoleSelectionWidget), m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignReplace), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), typeof(PawnRitualRoleSelectionWidget)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssignAnyRole), + typeof(PawnPsychicRitualRoleSelectionWidget)); Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), typeof(PawnRitualRoleSelectionWidget), m => m.TransformArgument(1, ritualRolesSerializer)); + Register(typeof(PawnRoleSelectionWidgetBase<>), nameof(PawnRoleSelectionWidgetBase.TryAssign), + typeof(PawnPsychicRitualRoleSelectionWidget), + m => m.TransformArgument(1, psychicRitualRolesSerializer)); Register( typeof(PawnRoleSelectionWidgetBase<>), @@ -83,6 +91,14 @@ void Register(Type baseType, string method, Type derivedType, Action), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 7).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Roles right click delegate (try assign spectate) Register( typeof(PawnRoleSelectionWidgetBase<>), @@ -92,5 +108,28 @@ void Register(Type baseType, string method, Type derivedType, Action), + MpMethodUtil.GetLambda( + typeof(PawnRoleSelectionWidgetBase), + nameof(PawnRoleSelectionWidgetBase.DrawPawnListInternal), + lambdaOrdinal: 2).Name, + typeof(PawnPsychicRitualRoleSelectionWidget) + ); // Not participating left click delegate (try assign any role or spectate) + } + + private static (WidgetType roleSelectionWidget, string Key) RitualRoleWriter(IEnumerable roles, object target, object[] _) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + var roleSelectionWidget = (WidgetType)target; + return (roleSelectionWidget, roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.SequenceEqual(roles))?.Key); + } + + private static IEnumerable RitualRoleReader((WidgetType roleSelectionWidget, string Key) data) + where WidgetType : PawnRoleSelectionWidgetBase + where RoleType : class, ILordJobRole + { + return data.roleSelectionWidget.assignments.RoleGroups().FirstOrDefault(g => g.Key == data.Key); } }