From 65c163f468b9ea19efd707aee370c5b8334c79fe Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 30 Sep 2023 21:23:18 +0200 Subject: [PATCH 01/29] Initial version of the sessions rework. Won't compile, as some things still need changing and/or adding. Some notes - A lot of cleanup needed, the classes implementing `ISession` and other interfaces are still missing several methods/properties - Some changes to trading sessions will be needed, as they aren't stored in a single trading-only-sessions list - `TickRateMultiplier` methods will need changing to actually use the reworked - `ISession` and the other interfaces will have to be moved to the API - The API should preferably expose abstract session classes to ensure any additions will be compatible in the future due to older implementations inheriting new members - The location (and existence) of `IIdBlockProvider` is subject to change - it may get removed, renamed, moved, etc. --- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 2 +- Source/Client/AsyncTime/TimeControlUI.cs | 55 +--- .../Client/Comp/Game/MultiplayerGameComp.cs | 3 +- Source/Client/Comp/IIdBlockProvider.cs | 8 + Source/Client/Comp/Map/MultiplayerMapComp.cs | 56 ++-- .../Client/Comp/World/MultiplayerWorldComp.cs | 49 +-- Source/Client/MultiplayerData.cs | 1 + Source/Client/MultiplayerGame.cs | 18 +- Source/Client/Patches/Patches.cs | 2 +- .../Persistent/CaravanFormingPatches.cs | 2 +- .../Client/Persistent/CaravanFormingProxy.cs | 2 +- .../Persistent/CaravanFormingSession.cs | 9 + .../Persistent/CaravanSplittingPatches.cs | 4 +- .../Persistent/CaravanSplittingSession.cs | 17 +- Source/Client/Persistent/ISession.cs | 32 +- Source/Client/Persistent/Rituals.cs | 24 +- Source/Client/Persistent/SessionManager.cs | 280 ++++++++++++++++++ Source/Client/Persistent/Trading.cs | 59 +++- .../Persistent/TransporterLoadingProxy.cs | 2 +- .../Persistent/TransporterLoadingSession.cs | 11 +- Source/Client/Saving/SemiPersistent.cs | 20 ++ Source/Client/Syncing/Dict/SyncDictDlc.cs | 4 +- Source/Client/Syncing/Game/SyncDelegates.cs | 6 +- Source/Client/Syncing/ImplSerialization.cs | 3 + 24 files changed, 518 insertions(+), 151 deletions(-) create mode 100644 Source/Client/Comp/IIdBlockProvider.cs create mode 100644 Source/Client/Persistent/SessionManager.cs diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index c9a6a421..be248fcd 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -102,7 +102,7 @@ public void Tick() { Find.TickManager.DoSingleTick(); worldTicks++; - Multiplayer.WorldComp.TickWorldTrading(); + Multiplayer.WorldComp.TickWorldSessions(); if (ModsConfig.BiotechActive) { diff --git a/Source/Client/AsyncTime/TimeControlUI.cs b/Source/Client/AsyncTime/TimeControlUI.cs index 4a88713c..380a334e 100644 --- a/Source/Client/AsyncTime/TimeControlUI.cs +++ b/Source/Client/AsyncTime/TimeControlUI.cs @@ -358,58 +358,9 @@ static void DrawWindowShortcuts(Rect button, Color bgColor, List GetBlockingWindowOptions(ColonistBar.Entry entry, ITickable tickable) { - List options = new List(); - var split = Multiplayer.WorldComp.splitSession; - - if (split != null && split.Caravan.pawns.Contains(entry.pawn)) - { - options.Add(new FloatMenuOption("MpCaravanSplittingSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - CameraJumper.TryJumpAndSelect(entry.pawn); - Multiplayer.WorldComp.splitSession.OpenWindow(); - })); - } - - if (Multiplayer.WorldComp.trading.FirstOrDefault(t => t.playerNegotiator?.Map == entry.map) is { } trade) - { - options.Add(new FloatMenuOption("MpTradingSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - CameraJumper.TryJumpAndSelect(trade.playerNegotiator); - Find.WindowStack.Add(new TradingWindow() - { selectedTab = Multiplayer.WorldComp.trading.IndexOf(trade) }); - })); - } - - if (entry.map?.MpComp().transporterLoading != null) - { - options.Add(new FloatMenuOption("MpTransportLoadingSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - entry.map.MpComp().transporterLoading.OpenWindow(); - })); - } - - if (entry.map?.MpComp().caravanForming != null) - { - options.Add(new FloatMenuOption("MpCaravanFormingSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - entry.map.MpComp().caravanForming.OpenWindow(); - })); - } - - if (entry.map?.MpComp().ritualSession != null) - { - options.Add(new FloatMenuOption("MpRitualSession".Translate(), () => - { - SwitchToMapOrWorld(entry.map); - entry.map.MpComp().ritualSession.OpenWindow(); - })); - } - - return options; + return Multiplayer.WorldComp.sessionManager.AllSessions + .ConcatIfNotNull(entry.map?.MpComp().sessionManager.AllSessions) + .Select(s => s.GetBlockingWindowOptions(entry)).Where(fmo => fmo != null).ToList(); } static void SwitchToMapOrWorld(Map map) diff --git a/Source/Client/Comp/Game/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs index 60abf1e5..55635fda 100644 --- a/Source/Client/Comp/Game/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Client.Comp { - public class MultiplayerGameComp : IExposable, IHasSemiPersistentData + public class MultiplayerGameComp : IExposable, IHasSemiPersistentData, IIdBlockProvider { public bool asyncTime; public bool debugMode; @@ -18,6 +18,7 @@ public class MultiplayerGameComp : IExposable, IHasSemiPersistentData public Dictionary playerData = new(); // player id to player data public IdBlock globalIdBlock = new(int.MaxValue / 2, 1_000_000_000); + public IdBlock IdBlock => globalIdBlock; public bool IsLowestWins => timeControl == TimeControl.LowestWins; diff --git a/Source/Client/Comp/IIdBlockProvider.cs b/Source/Client/Comp/IIdBlockProvider.cs new file mode 100644 index 00000000..248e2b17 --- /dev/null +++ b/Source/Client/Comp/IIdBlockProvider.cs @@ -0,0 +1,8 @@ +using Multiplayer.Common; + +namespace Multiplayer.Client.Comp; + +public interface IIdBlockProvider +{ + IdBlock IdBlock { get; } +} diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 8c650718..cf7f5617 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; +using Multiplayer.Client.Comp; using Multiplayer.Client.Persistent; using Multiplayer.Client.Saving; using Multiplayer.Common; @@ -11,7 +12,7 @@ namespace Multiplayer.Client { - public class MultiplayerMapComp : IExposable, IHasSemiPersistentData + public class MultiplayerMapComp : IExposable, IHasSemiPersistentData, IIdBlockProvider { public static bool tickingFactions; @@ -21,38 +22,55 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Dictionary factionData = new Dictionary(); public Dictionary customFactionData = new Dictionary(); - public CaravanFormingSession caravanForming; - public TransporterLoading transporterLoading; - public RitualSession ritualSession; + public SessionManager sessionManager; public List mapDialogs = new List(); public int autosaveCounter; // for SaveCompression public List tempLoadedThings; + // Using the global ID block since the map ID block was unused. + public IdBlock IdBlock => Multiplayer.GlobalIdBlock; + public MultiplayerMapComp(Map map) { this.map = map; + sessionManager = new SessionManager(this); } public CaravanFormingSession CreateCaravanFormingSession(bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? meetingSpot = null) { + var caravanForming = sessionManager.GetFirstOfType(); if (caravanForming == null) + { caravanForming = new CaravanFormingSession(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot); + if (!sessionManager.AddSession(caravanForming)) + return null; + } return caravanForming; } public TransporterLoading CreateTransporterLoadingSession(List transporters) { + var transporterLoading = sessionManager.GetFirstOfType(); if (transporterLoading == null) + { transporterLoading = new TransporterLoading(map, transporters); + if (!sessionManager.AddSession(transporterLoading)) + return null; + } return transporterLoading; } public RitualSession CreateRitualSession(RitualData data) { + var ritualSession = sessionManager.GetFirstOfType(); if (ritualSession == null) + { ritualSession = new RitualSession(map, data); + if (!sessionManager.AddSession(ritualSession)) + return null; + } return ritualSession; } @@ -114,8 +132,7 @@ public void ExposeData() Scribe_Values.Look(ref isPlayerHome, "isPlayerHome", false, true); } - Scribe_Deep.Look(ref caravanForming, "caravanFormingSession", map); - Scribe_Deep.Look(ref transporterLoading, "transporterLoading", map); + sessionManager.ExposeSessions(); Scribe_Collections.Look(ref mapDialogs, "mapDialogs", LookMode.Deep, map); if (Scribe.mode == LoadSaveMode.LoadingVars && mapDialogs == null) @@ -169,21 +186,14 @@ public void WriteSemiPersistent(ByteWriter writer) { writer.WriteInt32(autosaveCounter); - writer.WriteBool(ritualSession != null); - ritualSession?.Write(writer); + sessionManager.WriteSemiPersistent(writer); } - public void ReadSemiPersistent(ByteReader reader) + public void ReadSemiPersistent(ByteReader data) { - autosaveCounter = reader.ReadInt32(); + autosaveCounter = data.ReadInt32(); - var hasRitual = reader.ReadBool(); - if (hasRitual) - { - var session = new RitualSession(map); - session.Read(reader); - ritualSession = session; - } + sessionManager.ReadSemiPersistent(data); } } @@ -215,11 +225,11 @@ static void Prefix() if (Multiplayer.Client == null) return; // Trading window on resume save - if (Multiplayer.WorldComp.trading.NullOrEmpty()) return; - if (Multiplayer.WorldComp.trading.FirstOrDefault(t => t.playerNegotiator == null) is MpTradeSession trade) - { - trade.OpenWindow(); - } + if (!Multiplayer.WorldComp.sessionManager.AnySessionActive) return; + Multiplayer.WorldComp.sessionManager.AllSessions + .OfType() + .FirstOrDefault(t => t.playerNegotiator == null) + ?.OpenWindow(); } } @@ -233,7 +243,7 @@ static void Prefix(WorldInterface __instance) if (Find.World.renderer.wantedMode == WorldRenderMode.Planet) { // Hide trading window for map trades - if (Multiplayer.WorldComp.trading.All(t => t.playerNegotiator?.Map != null)) + if (Multiplayer.WorldComp.sessionManager.AllSessions.OfType().All(t => t.playerNegotiator?.Map != null)) { if (Find.WindowStack.IsOpen(typeof(TradingWindow))) Find.WindowStack.TryRemove(typeof(TradingWindow), doCloseSound: false); diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index e7ef795e..704e9542 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -1,28 +1,33 @@ using RimWorld; using RimWorld.Planet; using System.Collections.Generic; +using System.Linq; +using Multiplayer.Client.Comp; using Verse; using Multiplayer.Client.Persistent; using Multiplayer.Client.Saving; +using Multiplayer.Common; namespace Multiplayer.Client; -public class MultiplayerWorldComp +public class MultiplayerWorldComp : IHasSemiPersistentData, IIdBlockProvider { public Dictionary factionData = new(); public World world; public TileTemperaturesComp uiTemperatures; - public List trading = new(); - public CaravanSplittingSession splitSession; + public SessionManager sessionManager; private int currentFactionId; + public IdBlock IdBlock => Multiplayer.GlobalIdBlock; + public MultiplayerWorldComp(World world) { this.world = world; uiTemperatures = new TileTemperaturesComp(world); + sessionManager = new SessionManager(this); } // Called from AsyncWorldTimeComp.ExposeData @@ -30,12 +35,7 @@ public void ExposeData() { ExposeFactionData(); - Scribe_Collections.Look(ref trading, "tradingSessions", LookMode.Deep); - if (Scribe.mode == LoadSaveMode.PostLoadInit) - { - if (trading.RemoveAll(t => t.trader == null || t.playerNegotiator == null) > 0) - Log.Message("Some trading sessions had null entries"); - } + sessionManager.ExposeSessions(); } private void ExposeFactionData() @@ -71,23 +71,26 @@ private void ExposeFactionData() } } - public void TickWorldTrading() + public void WriteSemiPersistent(ByteWriter writer) { - for (int i = trading.Count - 1; i >= 0; i--) - { - var session = trading[i]; - if (session.playerNegotiator.Spawned) continue; + sessionManager.WriteSemiPersistent(writer); + } - if (session.ShouldCancel()) - RemoveTradeSession(session); - } + public void ReadSemiPersistent(ByteReader data) + { + sessionManager.ReadSemiPersistent(data); + } + + public void TickWorldSessions() + { + sessionManager.TickSessions(); } public void RemoveTradeSession(MpTradeSession session) { int index = trading.IndexOf(session); - trading.Remove(session); - Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); + trading.Remove(session); + Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); } public void SetFaction(Faction faction) @@ -108,7 +111,7 @@ public void SetFaction(Faction faction) public void DirtyColonyTradeForMap(Map map) { if (map == null) return; - foreach (MpTradeSession session in trading) + foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) if (session.playerNegotiator.Map == map) session.deal.recacheColony = true; } @@ -116,7 +119,7 @@ public void DirtyColonyTradeForMap(Map map) public void DirtyTraderTradeForTrader(ITrader trader) { if (trader == null) return; - foreach (MpTradeSession session in trading) + foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) if (session.trader == trader) session.deal.recacheTrader = true; } @@ -124,14 +127,14 @@ public void DirtyTraderTradeForTrader(ITrader trader) public void DirtyTradeForSpawnedThing(Thing t) { if (t is not { Spawned: true }) return; - foreach (MpTradeSession session in trading) + foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) if (session.playerNegotiator.Map == t.Map) session.deal.recacheThings.Add(t); } public bool AnyTradeSessionsOnMap(Map map) { - foreach (MpTradeSession session in trading) + foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) if (session.playerNegotiator.Map == map) return true; return false; diff --git a/Source/Client/MultiplayerData.cs b/Source/Client/MultiplayerData.cs index ceec4357..98cf9b4d 100644 --- a/Source/Client/MultiplayerData.cs +++ b/Source/Client/MultiplayerData.cs @@ -130,6 +130,7 @@ internal static void CollectDefInfos() dict["WorldComponent"] = GetDefInfo(RwImplSerialization.worldCompTypes, TypeHash); dict["MapComponent"] = GetDefInfo(RwImplSerialization.mapCompTypes, TypeHash); dict["ISyncSimple"] = GetDefInfo(ImplSerialization.syncSimples, TypeHash); + dict["ISession"] = GetDefInfo(ImplSerialization.sessions, TypeHash); dict["PawnBio"] = GetDefInfo(SolidBioDatabase.allBios, b => b.name.GetHashCode()); diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index a633647e..fe50fb33 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -112,23 +112,7 @@ public void OnDestroy() public IEnumerable GetSessions(Map map) { - foreach (var s in worldComp.trading) - yield return s; - - if (worldComp.splitSession != null) - yield return worldComp.splitSession; - - if (map == null) yield break; - var mapComp = map.MpComp(); - - if (mapComp.caravanForming != null) - yield return mapComp.caravanForming; - - if (mapComp.transporterLoading != null) - yield return mapComp.transporterLoading; - - if (mapComp.ritualSession != null) - yield return mapComp.ritualSession; + return worldComp.sessionManager.AllSessions.ConcatIfNotNull(map?.MpComp().sessionManager.AllSessions); } public void ChangeRealPlayerFaction(int newFaction) diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 53127bf5..c5c8555a 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -587,7 +587,7 @@ internal static void SyncDialogOptionByIndex(int position) if (!option.resolveTree) SyncUtil.isDialogNodeTreeOpen = true; // In case dialog is still open, we mark it as such // Try opening the trading menu if the picked option was supposed to do so (caravan meeting, trading option) - if (Multiplayer.Client != null && Multiplayer.WorldComp.trading.Any(t => t.trader is Caravan)) + if (Multiplayer.Client != null && Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader is Caravan)) Find.WindowStack.Add(new TradingWindow()); } else SyncUtil.isDialogNodeTreeOpen = false; diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index 1bc06c1b..510dfab2 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -166,7 +166,7 @@ static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action o if (Multiplayer.ExecutingCmds || Multiplayer.Ticking) { var comp = map.MpComp(); - if (comp.caravanForming == null) + if (comp.sessionManager.GetFirstOfType() == null) comp.CreateCaravanFormingSession(reform, onClosed, mapAboutToBeRemoved); } } diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs index fc3b02ee..dd442634 100644 --- a/Source/Client/Persistent/CaravanFormingProxy.cs +++ b/Source/Client/Persistent/CaravanFormingProxy.cs @@ -9,7 +9,7 @@ public class CaravanFormingProxy : Dialog_FormCaravan, ISwitchToMap { public static CaravanFormingProxy drawing; - public CaravanFormingSession Session => map.MpComp().caravanForming; + 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) { diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 8b381723..fd58a7b5 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -179,6 +179,15 @@ public void Notify_CountChanged(Transferable tr) { uiDirty = true; } + + public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpCaravanFormingSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } } } diff --git a/Source/Client/Persistent/CaravanSplittingPatches.cs b/Source/Client/Persistent/CaravanSplittingPatches.cs index 6588dfce..c0c62a5e 100644 --- a/Source/Client/Persistent/CaravanSplittingPatches.cs +++ b/Source/Client/Persistent/CaravanSplittingPatches.cs @@ -44,9 +44,9 @@ static bool Prefix(Caravan caravan) //Otherwise cancel creation of the Dialog_SplitCaravan. // If there's already an active session, open the window associated with it. // Otherwise, create a new session. - if (Multiplayer.WorldComp.splitSession != null) + if (Multiplayer.WorldComp.sessionManager.GetFirstOfType() is { } session) { - Multiplayer.WorldComp.splitSession.OpenWindow(); + session.OpenWindow(); } else { diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index a9ae5448..db261cd5 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -137,7 +137,7 @@ public void Notify_CountChanged(Transferable tr) [SyncMethod] public void CancelSplittingSession() { dialog.Close(); - Multiplayer.WorldComp.splitSession = null; + Multiplayer.WorldComp.sessionManager.RemoveSession(this); } /// @@ -161,8 +161,21 @@ public void AcceptSplitSession() { SoundDefOf.Tick_High.PlayOneShotOnCamera(); dialog.Close(false); - Multiplayer.WorldComp.splitSession = null; + Multiplayer.WorldComp.sessionManager.RemoveSession(this); } } + + public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + if (!Caravan.pawns.Contains(entry.pawn)) + return null; + + return new FloatMenuOption("MpCaravanSplittingSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + CameraJumper.TryJumpAndSelect(entry.pawn); + OpenWindow(); + }); + } } } diff --git a/Source/Client/Persistent/ISession.cs b/Source/Client/Persistent/ISession.cs index a9d3a390..56cda4ac 100644 --- a/Source/Client/Persistent/ISession.cs +++ b/Source/Client/Persistent/ISession.cs @@ -1,3 +1,4 @@ +using Multiplayer.API; using RimWorld; using Verse; @@ -7,7 +8,15 @@ public interface ISession { Map Map { get; } - int SessionId { get; } + int SessionId { get; set; } + + bool IsSessionValid { get; } + + bool IsCurrentlyPausingAll(); + + bool IsCurrentlyPausingMap(Map map); + + FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); } // todo unused for now @@ -22,4 +31,25 @@ public interface ISessionWithTransferables : ISession void Notify_CountChanged(Transferable tr); } + + public interface IExposableSession : ISession, IExposable + { + } + + public interface ISemiPersistentSession : ISession + { + void Write(SyncWorker sync); + + void Read(SyncWorker sync); + } + + public interface ISessionWithCreationRestrictions + { + bool CanExistWith(ISession other); + } + + public interface ITickingSession + { + void Tick(); + } } diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index feeab9fd..1eab585e 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -37,7 +37,7 @@ public RitualSession(Map map, RitualData data) [SyncMethod] public void Remove() { - map.MpComp().ritualSession = null; + map.MpComp().sessionManager.RemoveSession(this); } [SyncMethod] @@ -91,6 +91,15 @@ public void Read(ByteReader reader) data = SyncSerialization.ReadSync(reader); data.assignments.session = this; } + + public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpRitualSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } } public class MpRitualAssignments : RitualRoleAssignments @@ -102,7 +111,7 @@ public class BeginRitualProxy : Dialog_BeginRitual, ISwitchToMap { public static BeginRitualProxy drawing; - public RitualSession Session => map.MpComp().ritualSession; + public RitualSession Session => map.MpComp().sessionManager.GetFirstOfType(); public BeginRitualProxy(string header, string ritualLabel, Precept_Ritual ritual, TargetInfo target, Map map, ActionCallback action, Pawn organizer, RitualObligation obligation, Func filter = null, string confirmText = null, List requiredPawns = null, Dictionary forcedForRole = null, string ritualName = null, RitualOutcomeEffectDef outcome = null, List extraInfoText = null, Pawn selectedPawn = null) : base(header, ritualLabel, ritual, target, map, action, organizer, obligation, filter, confirmText, requiredPawns, forcedForRole, ritualName, outcome, extraInfoText, selectedPawn) { @@ -197,16 +206,17 @@ static bool Prefix(Window window) dialog.PostOpen(); // Completes initialization var comp = dialog.map.MpComp(); + var session = comp.sessionManager.GetFirstOfType(); - if (comp.ritualSession != null && - (comp.ritualSession.data.ritual != dialog.ritual || - comp.ritualSession.data.outcome != dialog.outcome)) + if (session != null && + (session.data.ritual != dialog.ritual || + session.data.outcome != dialog.outcome)) { Messages.Message("MpAnotherRitualInProgress".Translate(), MessageTypeDefOf.RejectInput, false); return false; } - if (comp.ritualSession == null) + if (session == null) { var data = new RitualData { @@ -226,7 +236,7 @@ static bool Prefix(Window window) } if (TickPatch.currentExecutingCmdIssuedBySelf) - comp.ritualSession.OpenWindow(); + session?.OpenWindow(); return false; } diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs new file mode 100644 index 00000000..d856bea9 --- /dev/null +++ b/Source/Client/Persistent/SessionManager.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Client.Comp; +using Multiplayer.Client.Saving; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client.Persistent; + +public class SessionManager : IHasSemiPersistentData +{ + public IReadOnlyList AllSessions => allSessions.AsReadOnly(); + public IReadOnlyList ExposableSessions => exposableSessions.AsReadOnly(); + public IReadOnlyList SemiPersistentSessions => semiPersistentSessions.AsReadOnly(); + public IReadOnlyList TickingSessions => tickingSessions.AsReadOnly(); + + private List allSessions = new(); + private List exposableSessions = new(); + private List semiPersistentSessions = new(); + private List tickingSessions = new(); + private static HashSet tempCleanupLoggingTypes = new(); + + private readonly IIdBlockProvider idBlockProvider; + + public bool AnySessionActive => allSessions.Count > 0; + + public SessionManager(IIdBlockProvider provider) + { + idBlockProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + /// Adds a new session to the list of active sessions. + /// + /// The session to try to add to active sessions. + /// if the session was added to active ones, if there was a conflict between sessions. + public bool AddSession(ISession session) + { + if (GetFirstConflictingSession(session) != null) + return false; + + AddSessionNoCheck(session); + return true; + } + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. + public ISession GetOrAddSessionAnyConflict(ISession session) + { + if (GetFirstConflictingSession(session) is { } other) + return other; + + AddSessionNoCheck(session); + return session; + } + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. + public T GetOrAddSession(T session) where T : ISession + { + if (session is ISessionWithCreationRestrictions s) + { + var conflicting = false; + + // Look for the first conflicting session of the same type as the input + foreach (var other in allSessions) + { + if (s.CanExistWith(other)) + continue; + // If we found a conflicting session of same type, return it + if (other is T o) + return o; + conflicting = true; + } + + // If there was a conflict but not of the same type as input, return null + if (conflicting) + return default; + } + + AddSessionNoCheck(session); + return session; + } + + private void AddSessionNoCheck(ISession session) + { + if (session is IExposableSession exposable) + exposableSessions.Add(exposable); + else if (session is ISemiPersistentSession semiPersistent) + semiPersistentSessions.Add(semiPersistent); + + if (session is ITickingSession ticking) + tickingSessions.Add(ticking); + + allSessions.Add(session); + session.SessionId = idBlockProvider.IdBlock.NextId(); + } + + /// + /// Tries to remove a session from active ones. + /// + /// The session to try to remove from the active sessions. + /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. + public bool RemoveSession(ISession session) + { + if (session is IExposableSession exposable) + exposableSessions.Remove(exposable); + else if (session is ISemiPersistentSession semiPersistent) + semiPersistentSessions.Remove(semiPersistent); + + if (session is ITickingSession ticking) + tickingSessions.Add(ticking); + + return allSessions.Remove(session); + } + + private ISession GetFirstConflictingSession(ISession session) + { + // Should the check be two-way? A property for optional two-way check? + if (session is ISessionWithCreationRestrictions restrictions) + return allSessions.FirstOrDefault(s => !restrictions.CanExistWith(s)); + + return null; + } + + /// + /// Ticks over . It iterates backwards using a loop to make it safe for the sessions to remove themselves when ticking. + /// + public void TickSessions() + { + for (int i = tickingSessions.Count - 1; i >= 0; i--) + tickingSessions[i].Tick(); + } + + public T GetFirstOfType() where T : ISession => allSessions.OfType().FirstOrDefault(); + + public T GetFirstWithId(int id) where T : ISession => allSessions.OfType().FirstOrDefault(s => s.SessionId == id); + + public ISession GetFirstWithId(int id) => allSessions.FirstOrDefault(s => s.SessionId == id); + + public void WriteSemiPersistent(ByteWriter data) + { + // Clear the set to make sure it's empty + tempCleanupLoggingTypes.Clear(); + for (int i = semiPersistentSessions.Count - 1; i >= 0; i--) + { + var session = semiPersistentSessions[i]; + if (!session.IsSessionValid) + { + semiPersistentSessions.RemoveAt(i); + allSessions.Remove(session); + var sessionType = session.GetType(); + if (!tempCleanupLoggingTypes.Add(sessionType)) + Log.Message($"Multiplayer session not valid after exposing data: {sessionType}"); + } + } + // Clear the set again to not leave behind any unneeded stuff + tempCleanupLoggingTypes.Clear(); + + data.WriteInt32(semiPersistentSessions.Count); + + foreach (var session in semiPersistentSessions) + { + data.WriteUShort((ushort)ImplSerialization.sessions.FindIndex(session.GetType())); + data.WriteInt32(session.Map?.uniqueID ?? -1); + + try + { + session.Write(new WritingSyncWorker(data)); + } + catch (Exception e) + { + Log.Error($"Trying to write semi persistent session for {session} failed with exception:\n{e}"); + } + } + } + + public void ReadSemiPersistent(ByteReader data) + { + var sessionsCount = data.ReadInt32(); + semiPersistentSessions.Clear(); + allSessions.RemoveAll(s => s is ISemiPersistentSession); + + for (int i = 0; i < sessionsCount; i++) + { + ushort typeIndex = data.ReadUShort(); + int mapId = data.ReadInt32(); + + if (typeIndex >= ImplSerialization.sessions.Length) + { + Log.Error($"Received data for ISession type with index out of range: {typeIndex}, session types count: {ImplSerialization.sessions.Length}"); + continue; + } + + var objType = ImplSerialization.sessions[typeIndex]; + Map map = null; + if (mapId != -1) + { + map = Find.Maps.FirstOrDefault(m => m.uniqueID == mapId); + if (map == null) + { + Log.Error($"Trying to read semi persistent session of type {objType} received a null map while expecting a map with ID {mapId}"); + // Continue? Let it run? + } + } + + try + { + if (Activator.CreateInstance(objType, map) is ISemiPersistentSession session) + { + session.Read(new ReadingSyncWorker(data)); + semiPersistentSessions.Add(session); + allSessions.Add(session); + } + } + catch (Exception e) + { + Log.Error($"Trying to read semi persistent session of type {objType} failed with exception:\n{e}"); + } + } + } + + public void ExposeSessions() + { + Scribe_Collections.Look(ref exposableSessions, "sessions", LookMode.Deep); + + if (Scribe.mode == LoadSaveMode.PostLoadInit) + { + allSessions ??= new(); + exposableSessions ??= new(); + semiPersistentSessions ??= new(); + + // Clear the set to make sure it's empty + tempCleanupLoggingTypes.Clear(); + for (int i = exposableSessions.Count - 1; i >= 0; i--) + { + var session = exposableSessions[i]; + if (!session.IsSessionValid) + { + // Removal from allSessions handled lower + exposableSessions.RemoveAt(i); + var sessionType = session.GetType(); + if (!tempCleanupLoggingTypes.Add(sessionType)) + Log.Message($"Multiplayer session not valid after exposing data: {sessionType}"); + } + } + // Clear the set again to not leave behind any unneeded stuff + tempCleanupLoggingTypes.Clear(); + + // Just in case something went wrong when exposing data, clear the all session from exposable ones and fill them again + allSessions.RemoveAll(s => s is IExposableSession); + allSessions.AddRange(exposableSessions); + } + } + + public static void ValidateSessionClasses() + { + foreach (var subclass in typeof(ISession).AllSubclasses()) + { + var interfaces = subclass.GetInterfaces(); + + if (interfaces.Contains(typeof(ISemiPersistentSession))) + { + if (interfaces.Contains(typeof(IExposableSession))) + Log.Error($"Type {subclass} implements both {nameof(IExposableSession)} and {nameof(ISemiPersistentSession)}, it should implement only one of them at most."); + + if (AccessTools.GetDeclaredConstructors(subclass).All(c => c.GetParameters().Length != 1 || c.GetParameters()[0].ParameterType != typeof(Map))) + Log.Error($"Type {subclass} implements {nameof(ISemiPersistentSession)}, but does not have a single parameter constructor with {nameof(Map)} as the parameter."); + } + } + } +} diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index b08c2b8e..d48d4a49 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -4,6 +4,7 @@ using RimWorld; using RimWorld.Planet; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Reflection.Emit; using Verse; @@ -12,7 +13,7 @@ namespace Multiplayer.Client { - public class MpTradeSession : IExposable, ISessionWithTransferables + public class MpTradeSession : IExposableSession, ISessionWithTransferables, ISessionWithCreationRestrictions, ITickingSession { public static MpTradeSession current; @@ -36,6 +37,8 @@ public string Label public Map Map => null; public int SessionId => sessionId; + public bool IsSessionValid => trader != null && playerNegotiator != null; + public MpTradeSession() { } private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) @@ -48,6 +51,21 @@ private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) giftsOnly = giftMode; } + public bool CanExistWith(ISession other) + { + if (other is not MpTradeSession otherTrade) + return true; + + // todo show error messages? + if (otherTrade.trader == trader) + return false; + + if (otherTrade.playerNegotiator == playerNegotiator) + return false; + + return true; + } + public static MpTradeSession TryCreate(ITrader trader, Pawn playerNegotiator, bool giftMode) { // todo show error messages? @@ -181,6 +199,17 @@ public void CloseWindow(bool sound = true) } } + public void Tick() + { + if (playerNegotiator.Spawned) return; + + if (ShouldCancel()) + { + Multiplayer.WorldComp.sessionManager.RemoveSession(this); + Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); + } + } + public void ExposeData() { Scribe_Values.Look(ref sessionId, "sessionId"); @@ -214,6 +243,20 @@ public void Notify_CountChanged(Transferable tr) { deal.caravanDirty = true; } + + public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + if (playerNegotiator?.Map != entry.map) + return null; + + return new FloatMenuOption("MpTradingSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + CameraJumper.TryJumpAndSelect(trade.playerNegotiator); + Find.WindowStack.Add(new TradingWindow() + { selectedTab = Multiplayer.WorldComp.trading.IndexOf(trade) }); + }); + } } @@ -548,9 +591,9 @@ static class OrbitalTradeBeaconPowerChanged static void Postfix(CompPowerTrader __instance, bool value) { if (Multiplayer.Client == null) return; - if (!(__instance.parent is Building_OrbitalTradeBeacon)) return; + if (__instance.parent is not Building_OrbitalTradeBeacon) return; if (value == __instance.powerOnInt) return; - if (!Multiplayer.WorldComp.trading.Any(t => t.trader is TradeShip)) return; + if (!Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader is TradeShip)) return; // For trade ships Multiplayer.WorldComp.DirtyColonyTradeForMap(__instance.parent.Map); @@ -675,15 +718,7 @@ static class DontDestroyStockWhileTrading { static bool Prefix(Settlement_TraderTracker __instance) { - if (Multiplayer.Client != null) - { - var trading = Multiplayer.WorldComp.trading; - for (int i = 0; i < trading.Count; i++) - if (trading[i].trader == __instance.settlement) - return false; - } - - return true; + return Multiplayer.Client == null || Multiplayer.WorldComp.sessionManager.AllSessions.OfType().All(s => s.trader != __instance.settlement); } } diff --git a/Source/Client/Persistent/TransporterLoadingProxy.cs b/Source/Client/Persistent/TransporterLoadingProxy.cs index 2ca7b0c8..d5b4f4ec 100644 --- a/Source/Client/Persistent/TransporterLoadingProxy.cs +++ b/Source/Client/Persistent/TransporterLoadingProxy.cs @@ -11,7 +11,7 @@ public class TransporterLoadingProxy : Dialog_LoadTransporters, ISwitchToMap public bool itemsReady; - public TransporterLoading Session => map.MpComp().transporterLoading; + public TransporterLoading Session => map.MpComp().sessionManager.GetFirstOfType(); public TransporterLoadingProxy(Map map, List transporters) : base(map, transporters) { diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index 8d95ebf2..0e433cf6 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -71,7 +71,7 @@ public void Reset() [SyncMethod] public void Remove() { - map.MpComp().transporterLoading = null; + map.MpComp().sessionManager.RemoveSession(this); } public void OpenWindow(bool sound = true) @@ -110,6 +110,15 @@ public void Notify_CountChanged(Transferable tr) { uiDirty = true; } + + public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return new FloatMenuOption("MpTransportLoadingSession".Translate(), () => + { + SwitchToMapOrWorld(entry.map); + OpenWindow(); + }); + } } } diff --git a/Source/Client/Saving/SemiPersistent.cs b/Source/Client/Saving/SemiPersistent.cs index 54e1f4da..d79ffaa9 100644 --- a/Source/Client/Saving/SemiPersistent.cs +++ b/Source/Client/Saving/SemiPersistent.cs @@ -32,6 +32,17 @@ public static byte[] WriteSemiPersistent() Log.Error($"Exception writing semi-persistent data for game: {e}"); } + try + { + var worldWriter = new ByteWriter(); + Multiplayer.WorldComp.WriteSemiPersistent(worldWriter); + writer.WritePrefixedBytes(worldWriter.ToArray()); + } + catch (Exception e) + { + Log.Error($"Exception writing semi-persistent data for world: {e}"); + } + writer.WriteInt32(Find.Maps.Count); foreach (var map in Find.Maps) { @@ -68,6 +79,15 @@ public static void ReadSemiPersistent(byte[] data) Log.Error($"Exception reading semi-persistent data for game: {e}"); } + try + { + Multiplayer.WorldComp.ReadSemiPersistent(new ByteReader(gameData)); + } + catch (Exception e) + { + Log.Error($"Exception reading semi-persistent data for world: {e}"); + } + var mapCount = reader.ReadInt32(); for (int i = 0; i < mapCount; i++) { diff --git a/Source/Client/Syncing/Dict/SyncDictDlc.cs b/Source/Client/Syncing/Dict/SyncDictDlc.cs index 5a48081a..b3eb6011 100644 --- a/Source/Client/Syncing/Dict/SyncDictDlc.cs +++ b/Source/Client/Syncing/Dict/SyncDictDlc.cs @@ -172,8 +172,8 @@ public static class SyncDictDlc }, (ByteReader data) => { var id = data.ReadInt32(); - var ritual = data.MpContext().map.MpComp().ritualSession; - return ritual?.SessionId == id ? ritual.data.assignments : null; + var ritual = data.MpContext().map.MpComp().sessionManager.GetFirstWithId(id); + return ritual?.data.assignments; } }, { diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 3b9f0112..21c085f2 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -453,8 +453,8 @@ private static void GizmoFormCaravan(Map map, bool reform, IntVec3? meetingSpot { var comp = map.MpComp(); - if (comp.caravanForming != null) - comp.caravanForming.OpenWindow(); + if (comp.sessionManager.GetFirstOfType() is { } session) + session.OpenWindow(); else CreateCaravanFormingSession(comp, reform, meetingSpot); } @@ -480,7 +480,7 @@ static void ReopenTradingWindowLocally(Caravan caravan, Command __result) ((Command_Action)__result).action = () => { - if (Multiplayer.Client != null && Multiplayer.WorldComp.trading.Any(t => t.trader == CaravanVisitUtility.SettlementVisitedNow(caravan))) + if (Multiplayer.Client != null && Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader == CaravanVisitUtility.SettlementVisitedNow(caravan))) { Find.WindowStack.Add(new TradingWindow()); return; diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index 661888dc..b44bda0a 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -1,5 +1,6 @@ using System; using Multiplayer.Client.Experimental; +using Multiplayer.Client.Persistent; using Multiplayer.Client.Util; namespace Multiplayer.Client; @@ -7,9 +8,11 @@ namespace Multiplayer.Client; public static class ImplSerialization { public static Type[] syncSimples; + public static Type[] sessions; public static void Init() { syncSimples = TypeUtil.AllImplementationsOrdered(typeof(ISyncSimple)); + sessions = TypeUtil.AllImplementationsOrdered(typeof(ISession)); } } From 2bfbf44f6e3df76742f2539115989931ccfbec6d Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Thu, 2 Nov 2023 20:07:12 +0100 Subject: [PATCH 02/29] First compiling version. Still WIP and needs cleaning. --- Source/Client/AsyncTime/AsyncTimeComp.cs | 15 +- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 4 +- .../Client/Comp/Game/MultiplayerGameComp.cs | 1 + Source/Client/Comp/IIdBlockProvider.cs | 8 - Source/Client/Comp/Map/MultiplayerMapComp.cs | 3 +- .../Client/Comp/World/MultiplayerWorldComp.cs | 7 +- Source/Client/Experimental/WindowSession.cs | 53 ++++++ .../Client/Persistent/CaravanFormingProxy.cs | 5 +- .../Persistent/CaravanFormingSession.cs | 24 +-- .../Persistent/CaravanSplittingPatches.cs | 5 +- .../Persistent/CaravanSplittingProxy.cs | 113 +++++++------ .../Persistent/CaravanSplittingSession.cs | 21 ++- Source/Client/Persistent/ISession.cs | 19 ++- Source/Client/Persistent/PauseLockSession.cs | 22 +++ Source/Client/Persistent/Rituals.cs | 10 +- Source/Client/Persistent/SessionManager.cs | 34 ++-- Source/Client/Persistent/Trading.cs | 36 ++-- Source/Client/Persistent/TradingUI.cs | 154 ++++++++++-------- .../Persistent/TransporterLoadingProxy.cs | 9 +- .../Persistent/TransporterLoadingSession.cs | 15 +- Source/Client/Syncing/Game/SyncFields.cs | 5 +- .../SyncSessionWithTransferablesMarker.cs | 21 +++ 22 files changed, 368 insertions(+), 216 deletions(-) delete mode 100644 Source/Client/Comp/IIdBlockProvider.cs create mode 100644 Source/Client/Experimental/WindowSession.cs create mode 100644 Source/Client/Persistent/PauseLockSession.cs create mode 100644 Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index a60c5ab1..399318bb 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -24,13 +24,16 @@ public float TickRateMultiplier(TimeSpeed speed) { var comp = map.MpComp(); - var enforcePause = comp.transporterLoading != null || - comp.caravanForming != null || - comp.ritualSession != null || - comp.mapDialogs.Any() || - Multiplayer.WorldComp.AnyTradeSessionsOnMap(map) || - Multiplayer.WorldComp.splitSession != null || + var enforcePause = comp.sessionManager.IsAnySessionCurrentlyPausing(map) || + Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(map) || pauseLocks.Any(x => x(map)); + // var enforcePause = comp.transporterLoading != null || + // comp.caravanForming != null || + // comp.ritualSession != null || + // comp.mapDialogs.Any() || + // Multiplayer.WorldComp.AnyTradeSessionsOnMap(map) || + // Multiplayer.WorldComp.splitSession != null || + // pauseLocks.Any(x => x(map)); if (enforcePause) return 0f; diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index 7de76a3d..7d7b967c 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -27,8 +27,10 @@ public float TickRateMultiplier(TimeSpeed speed) { if (Multiplayer.GameComp.asyncTime) { - var enforcePause = Multiplayer.WorldComp.splitSession != null || + var enforcePause = Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(null) || AsyncTimeComp.pauseLocks.Any(x => x(null)); + // var enforcePause = Multiplayer.WorldComp.splitSession != null || + // AsyncTimeComp.pauseLocks.Any(x => x(null)); if (enforcePause) return 0f; diff --git a/Source/Client/Comp/Game/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs index b3ab0122..47c7cc3f 100644 --- a/Source/Client/Comp/Game/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -17,6 +17,7 @@ public class MultiplayerGameComp : IExposable, IHasSemiPersistentData public PauseOnLetter pauseOnLetter; public TimeControl timeControl; public Dictionary playerData = new(); // player id to player data + public int nextSessionId; public string idBlockBase64; diff --git a/Source/Client/Comp/IIdBlockProvider.cs b/Source/Client/Comp/IIdBlockProvider.cs deleted file mode 100644 index 248e2b17..00000000 --- a/Source/Client/Comp/IIdBlockProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multiplayer.Common; - -namespace Multiplayer.Client.Comp; - -public interface IIdBlockProvider -{ - IdBlock IdBlock { get; } -} diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index e51ac8aa..c47467f1 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -23,7 +23,7 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Dictionary factionData = new(); public Dictionary customFactionData = new(); - public SessionManager sessionManager; + public SessionManager sessionManager = new(); public List mapDialogs = new List(); public int autosaveCounter; @@ -33,7 +33,6 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public MultiplayerMapComp(Map map) { this.map = map; - sessionManager = new SessionManager(this); } public CaravanFormingSession CreateCaravanFormingSession(bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? meetingSpot = null) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index 1f41eaed..07e21918 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -5,6 +5,7 @@ using Verse; using Multiplayer.Client.Persistent; using Multiplayer.Client.Saving; +using Multiplayer.Common; namespace Multiplayer.Client; @@ -15,7 +16,8 @@ public class MultiplayerWorldComp : IHasSemiPersistentData public World world; public TileTemperaturesComp uiTemperatures; - public SessionManager sessionManager; + public List trading = new(); // Should only be modified from MpTradeSession itself + public SessionManager sessionManager = new(); public Faction spectatorFaction; @@ -25,7 +27,6 @@ public MultiplayerWorldComp(World world) { this.world = world; uiTemperatures = new TileTemperaturesComp(world); - sessionManager = new SessionManager(this); } // Called from AsyncWorldTimeComp.ExposeData (for backcompat) @@ -173,7 +174,7 @@ public void DirtyTradeForSpawnedThing(Thing t) public bool AnyTradeSessionsOnMap(Map map) { - foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) + foreach (MpTradeSession session in trading.OfType()) if (session.playerNegotiator.Map == map) return true; return false; diff --git a/Source/Client/Experimental/WindowSession.cs b/Source/Client/Experimental/WindowSession.cs new file mode 100644 index 00000000..29d9c8cd --- /dev/null +++ b/Source/Client/Experimental/WindowSession.cs @@ -0,0 +1,53 @@ +using Multiplayer.Client.Persistent; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client.Experimental; + +// Abstract class for ease of updating API - making sure that adding more methods or properties to the +// interface won't cause issues with other mods by implementing them here as virtual methods. +public abstract class Session : ISession +{ + // Use internal to prevent mods from easily modifying it? + protected int sessionId; + // Should it be virtual? + public int SessionId + { + get => sessionId; + set => sessionId = value; + } + + public virtual bool IsSessionValid => true; + + // For subclasses implementing IExplosableSession + public virtual void ExposeData() + { + Scribe_Values.Look(ref sessionId, "sessionId"); + } + + public virtual void PostAddSession() + { + } + + public virtual void PostRemoveSession() + { + } + + protected static void SwitchToMapOrWorld(Map map) + { + if (map == null) + { + Find.World.renderer.wantedMode = WorldRenderMode.Planet; + } + else + { + if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); + Current.Game.CurrentMap = map; + } + } + + public abstract Map Map { get; } + public abstract bool IsCurrentlyPausing(Map map); + public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); +} diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs index dd442634..1e2068ca 100644 --- a/Source/Client/Persistent/CaravanFormingProxy.cs +++ b/Source/Client/Persistent/CaravanFormingProxy.cs @@ -17,12 +17,12 @@ public CaravanFormingProxy(Map map, bool reform = false, Action onClosed = null, public override void DoWindowContents(Rect inRect) { + var session = Session; + SyncSessionWithTransferablesMarker.DrawnThingFilter = session; drawing = this; try { - var session = Session; - if (session == null) { Close(); @@ -44,6 +44,7 @@ public override void DoWindowContents(Rect inRect) finally { drawing = null; + SyncSessionWithTransferablesMarker.DrawnThingFilter = null; } } } diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 195c96e1..e1d060dc 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -4,15 +4,15 @@ using RimWorld.Planet; using System; using System.Collections.Generic; +using Multiplayer.Client.Experimental; using Verse; namespace Multiplayer.Client { - public class CaravanFormingSession : IExposable, ISessionWithTransferables, IPausingWithDialog + public class CaravanFormingSession : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions { public Map map; - public int sessionId; public bool reform; public Action onClosed; public bool mapAboutToBeRemoved; @@ -24,8 +24,7 @@ public class CaravanFormingSession : IExposable, ISessionWithTransferables, IPau public bool uiDirty; - public Map Map => map; - public int SessionId => sessionId; + public override Map Map => map; public CaravanFormingSession(Map map) { @@ -34,14 +33,14 @@ public CaravanFormingSession(Map map) public CaravanFormingSession(Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? meetingSpot = null) : this(map) { - sessionId = Find.UniqueIDsManager.GetNextThingID(); - this.reform = reform; this.onClosed = onClosed; this.mapAboutToBeRemoved = mapAboutToBeRemoved; autoSelectTravelSupplies = !reform; this.meetingSpot = meetingSpot; + // Should this be called from PostAddSession? It would also be called from the other constructor + // (map only parameter) - do we want that to happen? Is it going to come up? AddItems(); } @@ -141,7 +140,7 @@ public void Reset() [SyncMethod] public void Remove() { - map.MpComp().caravanForming = null; + map.MpComp().sessionManager.RemoveSession(this); Find.WorldRoutePlanner.Stop(); } @@ -155,9 +154,10 @@ public void SetAutoSelectTravelSupplies(bool value) } } - public void ExposeData() + public override void ExposeData() { - Scribe_Values.Look(ref sessionId, "sessionId"); + base.ExposeData(); + Scribe_Values.Look(ref reform, "reform"); Scribe_Values.Look(ref onClosed, "onClosed"); Scribe_Values.Look(ref mapAboutToBeRemoved, "mapAboutToBeRemoved"); @@ -179,7 +179,9 @@ public void Notify_CountChanged(Transferable tr) uiDirty = true; } - public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) { return new FloatMenuOption("MpCaravanFormingSession".Translate(), () => { @@ -187,6 +189,8 @@ public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) OpenWindow(); }); } + + public bool CanExistWith(ISession other) => other is not CaravanFormingSession; } } diff --git a/Source/Client/Persistent/CaravanSplittingPatches.cs b/Source/Client/Persistent/CaravanSplittingPatches.cs index ab37a393..97ebacda 100644 --- a/Source/Client/Persistent/CaravanSplittingPatches.cs +++ b/Source/Client/Persistent/CaravanSplittingPatches.cs @@ -65,10 +65,7 @@ static bool Prefix(Caravan caravan) public static void CreateSplittingSession(Caravan caravan) { //Start caravan splitting session here by calling new session constructor - if (Multiplayer.WorldComp.splitSession == null) - { - Multiplayer.WorldComp.splitSession = new CaravanSplittingSession(caravan); - } + Multiplayer.WorldComp.sessionManager.AddSession(new CaravanSplittingSession(caravan)); } } } diff --git a/Source/Client/Persistent/CaravanSplittingProxy.cs b/Source/Client/Persistent/CaravanSplittingProxy.cs index 69108cdd..d8c462d9 100644 --- a/Source/Client/Persistent/CaravanSplittingProxy.cs +++ b/Source/Client/Persistent/CaravanSplittingProxy.cs @@ -44,64 +44,73 @@ public override void PostOpen() /// public override void DoWindowContents(Rect inRect) { - if (session == null) - { - Close(); - } - else if (session.uiDirty) + SyncSessionWithTransferablesMarker.DrawnThingFilter = session; + + try { - CountToTransferChanged(); + if (session == null) + { + Close(); + } + else if (session.uiDirty) + { + CountToTransferChanged(); - session.uiDirty = false; - } + session.uiDirty = false; + } - Rect rect = new Rect(0f, 0f, inRect.width, 35f); - Text.Font = GameFont.Medium; - Text.Anchor = TextAnchor.MiddleCenter; - Widgets.Label(rect, "SplitCaravan".Translate()); - Text.Font = GameFont.Small; - Text.Anchor = TextAnchor.UpperLeft; - CaravanUIUtility.DrawCaravanInfo(new CaravanUIUtility.CaravanInfo(SourceMassUsage, SourceMassCapacity, cachedSourceMassCapacityExplanation, SourceTilesPerDay, cachedSourceTilesPerDayExplanation, SourceDaysWorthOfFood, SourceForagedFoodPerDay, cachedSourceForagedFoodPerDayExplanation, SourceVisibility, cachedSourceVisibilityExplanation, -1f, -1f, null), new CaravanUIUtility.CaravanInfo(DestMassUsage, DestMassCapacity, cachedDestMassCapacityExplanation, DestTilesPerDay, cachedDestTilesPerDayExplanation, DestDaysWorthOfFood, DestForagedFoodPerDay, cachedDestForagedFoodPerDayExplanation, DestVisibility, cachedDestVisibilityExplanation, -1f, -1f, null), caravan.Tile, (!caravan.pather.Moving) ? null : new int?(TicksToArrive), -9999f, new Rect(12f, 35f, inRect.width - 24f, 40f), true, null, false); - tabsList.Clear(); - tabsList.Add(new TabRecord("PawnsTab".Translate(), delegate - { - tab = Tab.Pawns; - }, tab == Tab.Pawns)); - tabsList.Add(new TabRecord("ItemsTab".Translate(), delegate - { - tab = Tab.Items; - }, tab == Tab.Items)); - tabsList.Add(new TabRecord("FoodAndMedicineTab".Translate(), delegate - { - tab = Tab.FoodAndMedicine; - }, tab == Tab.FoodAndMedicine)); - inRect.yMin += 119f; - Widgets.DrawMenuSection(inRect); - TabDrawer.DrawTabs(inRect, tabsList, 200f); - inRect = inRect.ContractedBy(17f); - GUI.BeginGroup(inRect); - Rect rect2 = inRect.AtZero(); - DoBottomButtons(rect2); - Rect inRect2 = rect2; - inRect2.yMax -= 59f; - bool flag = false; - switch (tab) - { - case Tab.Pawns: - pawnsTransfer.OnGUI(inRect2, out flag); - break; - case Tab.Items: - itemsTransfer.OnGUI(inRect2, out flag); - break; - case Tab.FoodAndMedicine: - foodAndMedicineTransfer.OnGUI(inRect2, out flag); - break; + Rect rect = new Rect(0f, 0f, inRect.width, 35f); + Text.Font = GameFont.Medium; + Text.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(rect, "SplitCaravan".Translate()); + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + CaravanUIUtility.DrawCaravanInfo(new CaravanUIUtility.CaravanInfo(SourceMassUsage, SourceMassCapacity, cachedSourceMassCapacityExplanation, SourceTilesPerDay, cachedSourceTilesPerDayExplanation, SourceDaysWorthOfFood, SourceForagedFoodPerDay, cachedSourceForagedFoodPerDayExplanation, SourceVisibility, cachedSourceVisibilityExplanation, -1f, -1f, null), new CaravanUIUtility.CaravanInfo(DestMassUsage, DestMassCapacity, cachedDestMassCapacityExplanation, DestTilesPerDay, cachedDestTilesPerDayExplanation, DestDaysWorthOfFood, DestForagedFoodPerDay, cachedDestForagedFoodPerDayExplanation, DestVisibility, cachedDestVisibilityExplanation, -1f, -1f, null), caravan.Tile, (!caravan.pather.Moving) ? null : new int?(TicksToArrive), -9999f, new Rect(12f, 35f, inRect.width - 24f, 40f), true, null, false); + tabsList.Clear(); + tabsList.Add(new TabRecord("PawnsTab".Translate(), delegate + { + tab = Tab.Pawns; + }, tab == Tab.Pawns)); + tabsList.Add(new TabRecord("ItemsTab".Translate(), delegate + { + tab = Tab.Items; + }, tab == Tab.Items)); + tabsList.Add(new TabRecord("FoodAndMedicineTab".Translate(), delegate + { + tab = Tab.FoodAndMedicine; + }, tab == Tab.FoodAndMedicine)); + inRect.yMin += 119f; + Widgets.DrawMenuSection(inRect); + TabDrawer.DrawTabs(inRect, tabsList, 200f); + inRect = inRect.ContractedBy(17f); + GUI.BeginGroup(inRect); + Rect rect2 = inRect.AtZero(); + DoBottomButtons(rect2); + Rect inRect2 = rect2; + inRect2.yMax -= 59f; + bool flag = false; + switch (tab) + { + case Tab.Pawns: + pawnsTransfer.OnGUI(inRect2, out flag); + break; + case Tab.Items: + itemsTransfer.OnGUI(inRect2, out flag); + break; + case Tab.FoodAndMedicine: + foodAndMedicineTransfer.OnGUI(inRect2, out flag); + break; + } + if (flag) + { + CountToTransferChanged(); + } + GUI.EndGroup(); } - if (flag) + finally { - CountToTransferChanged(); + SyncSessionWithTransferablesMarker.DrawnThingFilter = null; } - GUI.EndGroup(); } /// diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index e6d2e824..106e1980 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -3,6 +3,7 @@ using RimWorld; using RimWorld.Planet; using Multiplayer.API; +using Multiplayer.Client.Experimental; using Verse.Sound; namespace Multiplayer.Client.Persistent @@ -10,15 +11,9 @@ namespace Multiplayer.Client.Persistent /// /// Represents an active Caravan Split session. This session will track all the pawns and items being split. /// - public class CaravanSplittingSession : IExposable, ISessionWithTransferables, IPausingWithDialog + public class CaravanSplittingSession : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions { - private int sessionId; - - /// - /// Uniquely identifies this ISessionWithTransferables - /// - public int SessionId => sessionId; - public Map Map => null; + public override Map Map => null; /// /// The list of items that can be transferred, along with their count. @@ -107,9 +102,9 @@ private CaravanSplittingProxy PrepareDialogProxy() return newProxy; } - public void ExposeData() + public override void ExposeData() { - Scribe_Values.Look(ref sessionId, "sessionId"); + base.ExposeData(); Scribe_Collections.Look(ref transferables, "transferables", LookMode.Deep); } @@ -164,7 +159,7 @@ public void AcceptSplitSession() } } - public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) { if (!Caravan.pawns.Contains(entry.pawn)) return null; @@ -176,5 +171,9 @@ public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) OpenWindow(); }); } + + public override bool IsCurrentlyPausing(Map map) => true; + + public bool CanExistWith(ISession other) => other is not CaravanSplittingSession; } } diff --git a/Source/Client/Persistent/ISession.cs b/Source/Client/Persistent/ISession.cs index 56cda4ac..0993da40 100644 --- a/Source/Client/Persistent/ISession.cs +++ b/Source/Client/Persistent/ISession.cs @@ -12,11 +12,18 @@ public interface ISession bool IsSessionValid { get; } - bool IsCurrentlyPausingAll(); - - bool IsCurrentlyPausingMap(Map map); + /// + /// + /// + /// Current map (when checked from local session manager) or (when checked from local session manager). + /// + bool IsCurrentlyPausing(Map map); FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); + + void PostAddSession(); + + void PostRemoveSession(); } // todo unused for now @@ -45,6 +52,12 @@ public interface ISemiPersistentSession : ISession public interface ISessionWithCreationRestrictions { + /// + /// Method used to check if the current session can be created by checking every other existing . + /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. + /// + /// The other session the current one is checked against. Can be of different type. + /// if the current session should be created, otherwise bool CanExistWith(ISession other); } diff --git a/Source/Client/Persistent/PauseLockSession.cs b/Source/Client/Persistent/PauseLockSession.cs new file mode 100644 index 00000000..4dac8995 --- /dev/null +++ b/Source/Client/Persistent/PauseLockSession.cs @@ -0,0 +1,22 @@ +using Multiplayer.Client.Experimental; +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Persistent; + +// Used for pause locks. Pause locks should become obsolete and this should become unused, +// but pause locks are kept for backwards compatibility. WIP. +public class PauseLockSession : Session +{ + public override Map Map => null; + + public override bool IsCurrentlyPausing(Map map) + { + throw new System.NotImplementedException(); + } + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + { + return null; + } +} diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index 266da2bb..47e11a7b 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Util; using UnityEngine; using Verse; @@ -12,13 +13,12 @@ namespace Multiplayer.Client.Persistent { - public class RitualSession : ISession, IPausingWithDialog + public class RitualSession : Session, IPausingWithDialog { public Map map; public RitualData data; - public Map Map => map; - public int SessionId { get; private set; } + public override Map Map => map; public RitualSession(Map map) { @@ -92,7 +92,9 @@ public void Read(ByteReader reader) data.assignments.session = this; } - public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) { return new FloatMenuOption("MpRitualSession".Translate(), () => { diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index d856bea9..4e37aae1 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; -using Multiplayer.Client.Comp; using Multiplayer.Client.Saving; using Multiplayer.Common; +using RimWorld; using Verse; namespace Multiplayer.Client.Persistent; @@ -22,15 +22,8 @@ public class SessionManager : IHasSemiPersistentData private List tickingSessions = new(); private static HashSet tempCleanupLoggingTypes = new(); - private readonly IIdBlockProvider idBlockProvider; - public bool AnySessionActive => allSessions.Count > 0; - public SessionManager(IIdBlockProvider provider) - { - idBlockProvider = provider ?? throw new ArgumentNullException(nameof(provider)); - } - /// /// Adds a new session to the list of active sessions. /// @@ -101,7 +94,8 @@ private void AddSessionNoCheck(ISession session) tickingSessions.Add(ticking); allSessions.Add(session); - session.SessionId = idBlockProvider.IdBlock.NextId(); + session.SessionId = UniqueIDsManager.GetNextID(ref Multiplayer.GameComp.nextSessionId); + session.PostAddSession(); } /// @@ -117,9 +111,16 @@ public bool RemoveSession(ISession session) semiPersistentSessions.Remove(semiPersistent); if (session is ITickingSession ticking) - tickingSessions.Add(ticking); + tickingSessions.Remove(ticking); + + if (allSessions.Remove(session)) + { + // Avoid repeated calls if the session was already removed + session.PostRemoveSession(); + return true; + } - return allSessions.Remove(session); + return false; } private ISession GetFirstConflictingSession(ISession session) @@ -261,6 +262,17 @@ public void ExposeSessions() } } + public bool IsAnySessionCurrentlyPausing(Map map) + { + for (int i = 0; i < allSessions.Count; i++) + { + if (AllSessions[i].IsCurrentlyPausing(map)) + return true; + } + + return false; + } + public static void ValidateSessionClasses() { foreach (var subclass in typeof(ISession).AllSubclasses()) diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 10c61934..0cebe3b2 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -7,17 +7,17 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using Multiplayer.Client.Experimental; using Verse; using Verse.AI; using Verse.AI.Group; namespace Multiplayer.Client { - public class MpTradeSession : IExposableSession, ISessionWithTransferables, ISessionWithCreationRestrictions, ITickingSession + public class MpTradeSession : Session, IExposableSession, ISessionWithTransferables, ISessionWithCreationRestrictions, ITickingSession { public static MpTradeSession current; - public int sessionId; public ITrader trader; public Pawn playerNegotiator; public bool giftMode; @@ -34,10 +34,9 @@ public string Label } } - public Map Map => playerNegotiator.Map; - public int SessionId => sessionId; + public override Map Map => playerNegotiator.Map; - public bool IsSessionValid => trader != null && playerNegotiator != null; + public override bool IsSessionValid => trader != null && playerNegotiator != null; public MpTradeSession() { } @@ -202,15 +201,12 @@ public void Tick() if (playerNegotiator.Spawned) return; if (ShouldCancel()) - { Multiplayer.WorldComp.sessionManager.RemoveSession(this); - Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); - } } - public void ExposeData() + public override void ExposeData() { - Scribe_Values.Look(ref sessionId, "sessionId"); + base.ExposeData(); ILoadReferenceable trader = (ILoadReferenceable)this.trader; Scribe_References.Look(ref trader, "trader"); @@ -221,6 +217,9 @@ public void ExposeData() Scribe_Values.Look(ref giftsOnly, "giftsOnly"); Scribe_Deep.Look(ref deal, "tradeDeal", this); + + if (Scribe.mode == LoadSaveMode.PostLoadInit) + Multiplayer.WorldComp.trading.AddDistinct(this); } public Transferable GetTransferableByThingId(int thingId) @@ -242,7 +241,9 @@ public void Notify_CountChanged(Transferable tr) deal.caravanDirty = true; } - public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + public override bool IsCurrentlyPausing(Map map) => map == Map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) { if (playerNegotiator?.Map != entry.map) return null; @@ -250,11 +251,20 @@ public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) return new FloatMenuOption("MpTradingSession".Translate(), () => { SwitchToMapOrWorld(entry.map); - CameraJumper.TryJumpAndSelect(trade.playerNegotiator); + CameraJumper.TryJumpAndSelect(playerNegotiator); Find.WindowStack.Add(new TradingWindow() - { selectedTab = Multiplayer.WorldComp.trading.IndexOf(trade) }); + { selectedTab = Multiplayer.WorldComp.trading.IndexOf(this) }); }); } + + public override void PostAddSession() => Multiplayer.WorldComp.trading.Add(this); + + public override void PostRemoveSession() + { + var index = Multiplayer.WorldComp.trading.IndexOf(this); + Multiplayer.WorldComp.trading.RemoveAt(index); + Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); + } } diff --git a/Source/Client/Persistent/TradingUI.cs b/Source/Client/Persistent/TradingUI.cs index 5a13d9c6..75b86969 100644 --- a/Source/Client/Persistent/TradingUI.cs +++ b/Source/Client/Persistent/TradingUI.cs @@ -1,3 +1,4 @@ +using System; using HarmonyLib; using Multiplayer.API; using RimWorld; @@ -34,93 +35,102 @@ public TradingWindow() public override void DoWindowContents(Rect inRect) { - added.RemoveAll(kv => Time.time - kv.Value > 1f); - removed.RemoveAll(kv => Time.time - kv.Value > 0.5f && RemoveCachedTradeable(kv.Key)); + SyncSessionWithTransferablesMarker.DrawnThingFilter = MpTradeSession.current; - tabs.Clear(); - var trading = Multiplayer.WorldComp.trading; - for (int i = 0; i < trading.Count; i++) + try { - int j = i; - tabs.Add(new TabRecord(trading[i].Label, () => selectedTab = j, () => selectedTab == j)); - } - - if (selectedTab == -1 && trading.Count > 0) - selectedTab = 0; - - if (selectedTab == -1) - { - Close(); - return; - } - - int rows = Mathf.CeilToInt(tabs.Count / 3f); - inRect.yMin += rows * TabDrawer.TabHeight + 3; - TabDrawer.DrawTabs(inRect, tabs, rows); - - inRect.yMin += 10f; - - var session = Multiplayer.WorldComp.trading[selectedTab]; - if (session.sessionId != selectedSession) - { - RecreateDialog(); - selectedSession = session.sessionId; - } - - { - MpTradeSession.SetTradeSession(session); - drawingTrade = this; - - if (session.deal.ShouldRecache) - session.deal.Recache(); + added.RemoveAll(kv => Time.time - kv.Value > 1f); + removed.RemoveAll(kv => Time.time - kv.Value > 0.5f && RemoveCachedTradeable(kv.Key)); - if (session.deal.uiShouldReset != UIShouldReset.None) + tabs.Clear(); + var trading = Multiplayer.WorldComp.trading; + for (int i = 0; i < trading.Count; i++) { - if (session.deal.uiShouldReset != UIShouldReset.Silent) - BeforeCache(); - - dialog.CacheTradeables(); - dialog.CountToTransferChanged(); - - session.deal.uiShouldReset = UIShouldReset.None; + int j = i; + tabs.Add(new TabRecord(trading[i].Label, () => selectedTab = j, () => selectedTab == j)); } - if (session.deal.caravanDirty) - { - dialog.CountToTransferChanged(); - session.deal.caravanDirty = false; - } + if (selectedTab == -1 && trading.Count > 0) + selectedTab = 0; - GUI.BeginGroup(inRect); + if (selectedTab == -1) { - Rect groupRect = new Rect(0, 0, inRect.width, inRect.height); - dialog.DoWindowContents(groupRect); + Close(); + return; } - GUI.EndGroup(); - int? traderLeavingIn = GetTraderTime(TradeSession.trader); - if (traderLeavingIn != null) + int rows = Mathf.CeilToInt(tabs.Count / 3f); + inRect.yMin += rows * TabDrawer.TabHeight + 3; + TabDrawer.DrawTabs(inRect, tabs, rows); + + inRect.yMin += 10f; + + var session = Multiplayer.WorldComp.trading[selectedTab]; + if (session.SessionId != selectedSession) { - float num = inRect.width - 590f; - Rect position = new Rect(inRect.x + num, inRect.y, inRect.width - num, 58f); - Rect traderNameRect = new Rect(position.x + position.width / 2f, position.y, position.width / 2f - 1f, position.height); - Rect traderTimeRect = traderNameRect.Up(traderNameRect.height - 5f); - - Text.Anchor = TextAnchor.LowerRight; - Widgets.Label(traderTimeRect, "MpTraderLeavesIn".Translate(traderLeavingIn?.ToStringTicksToPeriod())); - Text.Anchor = TextAnchor.UpperLeft; + RecreateDialog(); + selectedSession = session.SessionId; } - if (cancelPressed) { - CancelTradeSession(session); - cancelPressed = false; + MpTradeSession.SetTradeSession(session); + drawingTrade = this; + + if (session.deal.ShouldRecache) + session.deal.Recache(); + + if (session.deal.uiShouldReset != UIShouldReset.None) + { + if (session.deal.uiShouldReset != UIShouldReset.Silent) + BeforeCache(); + + dialog.CacheTradeables(); + dialog.CountToTransferChanged(); + + session.deal.uiShouldReset = UIShouldReset.None; + } + + if (session.deal.caravanDirty) + { + dialog.CountToTransferChanged(); + session.deal.caravanDirty = false; + } + + GUI.BeginGroup(inRect); + { + Rect groupRect = new Rect(0, 0, inRect.width, inRect.height); + dialog.DoWindowContents(groupRect); + } + GUI.EndGroup(); + + int? traderLeavingIn = GetTraderTime(TradeSession.trader); + if (traderLeavingIn != null) + { + float num = inRect.width - 590f; + Rect position = new Rect(inRect.x + num, inRect.y, inRect.width - num, 58f); + Rect traderNameRect = new Rect(position.x + position.width / 2f, position.y, position.width / 2f - 1f, position.height); + Rect traderTimeRect = traderNameRect.Up(traderNameRect.height - 5f); + + Text.Anchor = TextAnchor.LowerRight; + Widgets.Label(traderTimeRect, "MpTraderLeavesIn".Translate(traderLeavingIn?.ToStringTicksToPeriod())); + Text.Anchor = TextAnchor.UpperLeft; + } + + if (cancelPressed) + { + CancelTradeSession(session); + cancelPressed = false; + } + + session.giftMode = TradeSession.giftMode; + + drawingTrade = null; + MpTradeSession.SetTradeSession(null); } - - session.giftMode = TradeSession.giftMode; - - drawingTrade = null; - MpTradeSession.SetTradeSession(null); + } + finally + { + SyncSessionWithTransferablesMarker.DrawnThingFilter = null; } } diff --git a/Source/Client/Persistent/TransporterLoadingProxy.cs b/Source/Client/Persistent/TransporterLoadingProxy.cs index d5b4f4ec..e493c86e 100644 --- a/Source/Client/Persistent/TransporterLoadingProxy.cs +++ b/Source/Client/Persistent/TransporterLoadingProxy.cs @@ -7,10 +7,10 @@ namespace Multiplayer.Client { public class TransporterLoadingProxy : Dialog_LoadTransporters, ISwitchToMap { - public static TransporterLoadingProxy drawing; - public bool itemsReady; + public static TransporterLoadingProxy drawing; + public TransporterLoading Session => map.MpComp().sessionManager.GetFirstOfType(); public TransporterLoadingProxy(Map map, List transporters) : base(map, transporters) @@ -19,12 +19,12 @@ public TransporterLoadingProxy(Map map, List transporters) : ba public override void DoWindowContents(Rect inRect) { + var session = Session; + SyncSessionWithTransferablesMarker.DrawnThingFilter = session; drawing = this; try { - var session = Session; - if (session == null) { Close(); @@ -40,6 +40,7 @@ public override void DoWindowContents(Rect inRect) finally { drawing = null; + SyncSessionWithTransferablesMarker.DrawnThingFilter = null; } } } diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index 63633677..f208ccba 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -1,18 +1,17 @@ using System.Collections.Generic; using System.Linq; using Multiplayer.API; +using Multiplayer.Client.Experimental; using RimWorld; using Verse; using Multiplayer.Client.Persistent; namespace Multiplayer.Client { - public class TransporterLoading : IExposable, ISessionWithTransferables, IPausingWithDialog + public class TransporterLoading : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog { - public int SessionId => sessionId; - public Map Map => map; + public override Map Map => map; - public int sessionId; public Map map; public List transporters; @@ -92,8 +91,10 @@ private TransporterLoadingProxy PrepareDummyDialog() }; } - public void ExposeData() + public override void ExposeData() { + base.ExposeData(); + Scribe_Collections.Look(ref transferables, "transferables", LookMode.Deep); Scribe_Collections.Look(ref pods, "transporters", LookMode.Reference); @@ -111,7 +112,9 @@ public void Notify_CountChanged(Transferable tr) uiDirty = true; } - public FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) + public override bool IsCurrentlyPausing(Map map) => map == this.map; + + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) { return new FloatMenuOption("MpTransportLoadingSession".Translate(), () => { diff --git a/Source/Client/Syncing/Game/SyncFields.cs b/Source/Client/Syncing/Game/SyncFields.cs index 3cb25562..091973cc 100644 --- a/Source/Client/Syncing/Game/SyncFields.cs +++ b/Source/Client/Syncing/Game/SyncFields.cs @@ -449,10 +449,7 @@ static void RenameZone(Dialog_RenameZone __instance) [MpPrefix(typeof(TransferableUIUtility), "DoCountAdjustInterface")] static void TransferableAdjustTo(Transferable trad) { - var session = MpTradeSession.current ?? - (Multiplayer.Client != null ? Multiplayer.WorldComp.splitSession : null) ?? - (ISessionWithTransferables)CaravanFormingProxy.drawing?.Session ?? - TransporterLoadingProxy.drawing?.Session; + var session = SyncSessionWithTransferablesMarker.DrawnThingFilter; if (session != null) SyncTradeableCount.Watch(new MpTransferableReference(session, trad)); } diff --git a/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs b/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs new file mode 100644 index 00000000..b0957e2f --- /dev/null +++ b/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs @@ -0,0 +1,21 @@ +using System; +using Multiplayer.Client.Persistent; + +namespace Multiplayer.Client; + +public class SyncSessionWithTransferablesMarker +{ + private static ISessionWithTransferables thingFilterContext; + + public static ISessionWithTransferables DrawnThingFilter + { + get => thingFilterContext; + set + { + if (value != null && thingFilterContext != null) + throw new Exception("Session with transferables context already set!"); + + thingFilterContext = value; + } + } +} From 25982734956122692f9f78963a21d47034e3f4c3 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Fri, 3 Nov 2023 16:36:37 +0100 Subject: [PATCH 03/29] Turn pause locks into a session - The session is only added if there's any pause lock added by a mod - The session will only pause itself if any of the pause locks does so as well - Once we update the API to include sessions, pause locks should be marked as obsolete --- Source/Client/AsyncTime/AsyncTimeComp.cs | 4 +--- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 3 +-- Source/Client/MultiplayerAPIBridge.cs | 2 +- Source/Client/Persistent/PauseLockSession.cs | 19 +++++++++---------- Source/Client/Syncing/Sync.cs | 16 +++++++++++++++- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 399318bb..d811563a 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -18,15 +18,13 @@ public class AsyncTimeComp : IExposable, ITickable { public static Map tickingMap; public static Map executingCmdMap; - public static List pauseLocks = new(); public float TickRateMultiplier(TimeSpeed speed) { var comp = map.MpComp(); var enforcePause = comp.sessionManager.IsAnySessionCurrentlyPausing(map) || - Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(map) || - pauseLocks.Any(x => x(map)); + Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(map); // var enforcePause = comp.transporterLoading != null || // comp.caravanForming != null || // comp.ritualSession != null || diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index 7d7b967c..a576d3f7 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -27,8 +27,7 @@ public float TickRateMultiplier(TimeSpeed speed) { if (Multiplayer.GameComp.asyncTime) { - var enforcePause = Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(null) || - AsyncTimeComp.pauseLocks.Any(x => x(null)); + var enforcePause = Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(null); // var enforcePause = Multiplayer.WorldComp.splitSession != null || // AsyncTimeComp.pauseLocks.Any(x => x(null)); diff --git a/Source/Client/MultiplayerAPIBridge.cs b/Source/Client/MultiplayerAPIBridge.cs index a6a05727..af8c7664 100644 --- a/Source/Client/MultiplayerAPIBridge.cs +++ b/Source/Client/MultiplayerAPIBridge.cs @@ -117,7 +117,7 @@ public void RegisterDialogNodeTree(MethodInfo method) public void RegisterPauseLock(PauseLockDelegate pauseLock) { - AsyncTimeComp.pauseLocks.Add(pauseLock); + Sync.RegisterPauseLock(pauseLock); } } } diff --git a/Source/Client/Persistent/PauseLockSession.cs b/Source/Client/Persistent/PauseLockSession.cs index 4dac8995..0f85cc33 100644 --- a/Source/Client/Persistent/PauseLockSession.cs +++ b/Source/Client/Persistent/PauseLockSession.cs @@ -1,22 +1,21 @@ -using Multiplayer.Client.Experimental; +using System.Collections.Generic; +using Multiplayer.API; +using Multiplayer.Client.Experimental; using RimWorld; using Verse; namespace Multiplayer.Client.Persistent; // Used for pause locks. Pause locks should become obsolete and this should become unused, -// but pause locks are kept for backwards compatibility. WIP. +// but pause locks are kept for backwards compatibility. public class PauseLockSession : Session { + public List pauseLocks = new(); + public override Map Map => null; - public override bool IsCurrentlyPausing(Map map) - { - throw new System.NotImplementedException(); - } + public override bool IsCurrentlyPausing(Map map) => pauseLocks.Any(x => x(map)); - public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) - { - return null; - } + // Should we add some message explaining pause locks/having a list of pausing ones? + public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) => null; } diff --git a/Source/Client/Syncing/Sync.cs b/Source/Client/Syncing/Sync.cs index d50e81e4..4dc41823 100644 --- a/Source/Client/Syncing/Sync.cs +++ b/Source/Client/Syncing/Sync.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Multiplayer.Client.Persistent; using Verse; namespace Multiplayer.Client @@ -375,7 +376,20 @@ public static void RegisterPauseLock(MethodInfo method) if (pauseLock == null) throw new Exception($"Couldn't generate pause lock delegate from {method.DeclaringType?.FullName}:{method.Name}"); - AsyncTimeComp.pauseLocks.Add(pauseLock); + RegisterPauseLock(pauseLock); + } + + public static void RegisterPauseLock(PauseLockDelegate pauseLock) + { + var session = Multiplayer.WorldComp.sessionManager.GetFirstOfType(); + if (session == null) + { + // Only ever add pause lock session if we have any pause locks + session = new PauseLockSession(); + Multiplayer.WorldComp.sessionManager.AddSession(session); + } + + session.pauseLocks.Add(pauseLock); } public static void ValidateAll() From cd03e8cb419be8657776fb6c8ad4260fda9f869d Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Fri, 3 Nov 2023 16:38:45 +0100 Subject: [PATCH 04/29] Some minor cleanup related to trade sessions --- Source/Client/Comp/World/MultiplayerWorldComp.cs | 11 +++++++---- Source/Client/Persistent/Trading.cs | 4 ++-- Source/Client/Syncing/Game/SyncDelegates.cs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index 07e21918..eb0937d0 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -16,7 +16,7 @@ public class MultiplayerWorldComp : IHasSemiPersistentData public World world; public TileTemperaturesComp uiTemperatures; - public List trading = new(); // Should only be modified from MpTradeSession itself + public List trading = new(); // Should only be modified from MpTradeSession in PostAdd/Remove and ExposeData public SessionManager sessionManager = new(); public Faction spectatorFaction; @@ -36,7 +36,11 @@ public void ExposeData() Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); + if (Scribe.mode == LoadSaveMode.PostLoadInit) + trading.Clear(); // Reset the list, let the sessions re-add themselves from ExposeData - should prevent any potential issues with this list having different order or elements sessionManager.ExposeSessions(); + if (Scribe.mode == LoadSaveMode.PostLoadInit && MpTradeSession.current != null && TradingWindow.drawingTrade != null) + TradingWindow.drawingTrade.selectedTab = trading.IndexOf(MpTradeSession.current); // In case order changed, set the current tab DoBackCompat(); } @@ -128,9 +132,8 @@ public void TickWorldSessions() public void RemoveTradeSession(MpTradeSession session) { - int index = trading.IndexOf(session); - trading.Remove(session); - Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); + // Cleanup and removal from `trading` field is handled in PostRemoveSession + sessionManager.RemoveSession(session); } public void SetFaction(Faction faction) diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 0cebe3b2..04363c67 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -601,7 +601,7 @@ static void Postfix(CompPowerTrader __instance, bool value) if (Multiplayer.Client == null) return; if (__instance.parent is not Building_OrbitalTradeBeacon) return; if (value == __instance.powerOnInt) return; - if (!Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader is TradeShip)) return; + if (!Multiplayer.WorldComp.trading.Any(t => t.trader is TradeShip)) return; // For trade ships Multiplayer.WorldComp.DirtyColonyTradeForMap(__instance.parent.Map); @@ -726,7 +726,7 @@ static class DontDestroyStockWhileTrading { static bool Prefix(Settlement_TraderTracker __instance) { - return Multiplayer.Client == null || Multiplayer.WorldComp.sessionManager.AllSessions.OfType().All(s => s.trader != __instance.settlement); + return Multiplayer.Client == null || Multiplayer.WorldComp.trading.All(s => s.trader != __instance.settlement); } } diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 95151246..f1e9008a 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -479,7 +479,7 @@ static void ReopenTradingWindowLocally(Caravan caravan, Command __result) ((Command_Action)__result).action = () => { - if (Multiplayer.Client != null && Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader == CaravanVisitUtility.SettlementVisitedNow(caravan))) + if (Multiplayer.Client != null && Multiplayer.WorldComp.trading.Any(t => t.trader == CaravanVisitUtility.SettlementVisitedNow(caravan))) { Find.WindowStack.Add(new TradingWindow()); return; From 95613604fc7f94e0bf959a5cbf9c81764e3bbc7f Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 4 Nov 2023 19:24:21 +0100 Subject: [PATCH 05/29] MpTradeSession.TryCreate now uses session manager --- Source/Client/Persistent/Trading.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 04363c67..271b0c22 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -67,15 +67,10 @@ public bool CanExistWith(ISession other) public static MpTradeSession TryCreate(ITrader trader, Pawn playerNegotiator, bool giftMode) { - // todo show error messages? - if (Multiplayer.WorldComp.trading.Any(s => s.trader == trader)) - return null; - - if (Multiplayer.WorldComp.trading.Any(s => s.playerNegotiator == playerNegotiator)) - return null; - MpTradeSession session = new MpTradeSession(trader, playerNegotiator, giftMode); - Multiplayer.WorldComp.trading.Add(session); + // Return null if there was a conflicting session + if (!Multiplayer.WorldComp.sessionManager.AddSession(session)) + return null; CancelTradeDealReset.cancel = true; SetTradeSession(session); From 9814dd03db2b1136ce0210323253c6f074800ba3 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 4 Nov 2023 19:25:18 +0100 Subject: [PATCH 06/29] Use `WorldComp.trading` instead of `WorldComp.sessionManager` --- Source/Client/Comp/Map/MultiplayerMapComp.cs | 9 +++------ Source/Client/Comp/World/MultiplayerWorldComp.cs | 8 ++++---- Source/Client/Patches/Patches.cs | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index c47467f1..2e2b6fdc 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -224,12 +224,9 @@ static void Prefix() if (Multiplayer.Client == null) return; // Trading window on resume save - if (!Multiplayer.WorldComp.sessionManager.AnySessionActive) return; + if (!Multiplayer.WorldComp.trading.NullOrEmpty()) return; // playerNegotiator == null can only happen during loading? Is this a resuming check? - Multiplayer.WorldComp.sessionManager.AllSessions - .OfType() - .FirstOrDefault(t => t.playerNegotiator == null) - ?.OpenWindow(); + Multiplayer.WorldComp.trading.FirstOrDefault(t => t.playerNegotiator == null)?.OpenWindow(); } } @@ -243,7 +240,7 @@ static void Prefix(WorldInterface __instance) if (Find.World.renderer.wantedMode == WorldRenderMode.Planet) { // Hide trading window for map trades - if (Multiplayer.WorldComp.sessionManager.AllSessions.OfType().All(t => t.playerNegotiator?.Map != null)) + if (Multiplayer.WorldComp.trading.All(t => t.playerNegotiator?.Map != null)) { if (Find.WindowStack.IsOpen(typeof(TradingWindow))) Find.WindowStack.TryRemove(typeof(TradingWindow), doCloseSound: false); diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index eb0937d0..6c500ff9 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -154,7 +154,7 @@ public void SetFaction(Faction faction) public void DirtyColonyTradeForMap(Map map) { if (map == null) return; - foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) + foreach (MpTradeSession session in trading) if (session.playerNegotiator.Map == map) session.deal.recacheColony = true; } @@ -162,7 +162,7 @@ public void DirtyColonyTradeForMap(Map map) public void DirtyTraderTradeForTrader(ITrader trader) { if (trader == null) return; - foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) + foreach (MpTradeSession session in trading) if (session.trader == trader) session.deal.recacheTrader = true; } @@ -170,14 +170,14 @@ public void DirtyTraderTradeForTrader(ITrader trader) public void DirtyTradeForSpawnedThing(Thing t) { if (t is not { Spawned: true }) return; - foreach (MpTradeSession session in sessionManager.AllSessions.OfType()) + foreach (MpTradeSession session in trading) if (session.playerNegotiator.Map == t.Map) session.deal.recacheThings.Add(t); } public bool AnyTradeSessionsOnMap(Map map) { - foreach (MpTradeSession session in trading.OfType()) + foreach (MpTradeSession session in trading) if (session.playerNegotiator.Map == map) return true; return false; diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index ab0ee934..175062bf 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -488,7 +488,7 @@ internal static void SyncDialogOptionByIndex(int position) if (!option.resolveTree) SyncUtil.isDialogNodeTreeOpen = true; // In case dialog is still open, we mark it as such // Try opening the trading menu if the picked option was supposed to do so (caravan meeting, trading option) - if (Multiplayer.Client != null && Multiplayer.WorldComp.sessionManager.AllSessions.OfType().Any(t => t.trader is Caravan)) + if (Multiplayer.Client != null && Multiplayer.WorldComp.trading.Any(t => t.trader is Caravan)) Find.WindowStack.Add(new TradingWindow()); } else SyncUtil.isDialogNodeTreeOpen = false; From 23b6a96a3802742bb9e07b23deb40801833ff37e Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 4 Nov 2023 19:25:39 +0100 Subject: [PATCH 07/29] Minor cleanup and error logging --- Source/Client/Comp/Map/MultiplayerMapComp.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 2e2b6fdc..f9b4a26b 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -24,7 +24,7 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Dictionary customFactionData = new(); public SessionManager sessionManager = new(); - public List mapDialogs = new List(); + public List mapDialogs = new(); public int autosaveCounter; // for SaveCompression @@ -42,7 +42,11 @@ public CaravanFormingSession CreateCaravanFormingSession(bool reform, Action onC { caravanForming = new CaravanFormingSession(map, reform, onClosed, mapAboutToBeRemoved, meetingSpot); if (!sessionManager.AddSession(caravanForming)) + { + // Shouldn't happen if the session doesn't exist already, show an error just in case + Log.Error($"Failed trying to created a session of type {nameof(CaravanFormingSession)} - prior session did not exist and creating session failed."); return null; + } } return caravanForming; } @@ -54,7 +58,11 @@ public TransporterLoading CreateTransporterLoadingSession(List { transporterLoading = new TransporterLoading(map, transporters); if (!sessionManager.AddSession(transporterLoading)) + { + // Shouldn't happen if the session doesn't exist already, show an error just in case + Log.Error($"Failed trying to created a session of type {nameof(TransporterLoading)} - prior session did not exist and creating session failed."); return null; + } } return transporterLoading; } @@ -66,7 +74,11 @@ public RitualSession CreateRitualSession(RitualData data) { ritualSession = new RitualSession(map, data); if (!sessionManager.AddSession(ritualSession)) + { + // Shouldn't happen if the session doesn't exist already, show an error just in case + Log.Error($"Failed trying to created a session of type {nameof(RitualSession)} - prior session did not exist and creating session failed."); return null; + } } return ritualSession; } @@ -188,11 +200,11 @@ public void WriteSemiPersistent(ByteWriter writer) sessionManager.WriteSemiPersistent(writer); } - public void ReadSemiPersistent(ByteReader data) + public void ReadSemiPersistent(ByteReader reader) { - autosaveCounter = data.ReadInt32(); + autosaveCounter = reader.ReadInt32(); - sessionManager.ReadSemiPersistent(data); + sessionManager.ReadSemiPersistent(reader); } } From d4f363ce1fe66bbf8044bc3d8cde7aed018e588d Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 4 Nov 2023 19:46:37 +0100 Subject: [PATCH 08/29] Fixed reading world data in `SemiPersistent:ReadSemiPersistent` --- Source/Client/Saving/SemiPersistent.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Client/Saving/SemiPersistent.cs b/Source/Client/Saving/SemiPersistent.cs index d79ffaa9..617bd382 100644 --- a/Source/Client/Saving/SemiPersistent.cs +++ b/Source/Client/Saving/SemiPersistent.cs @@ -79,9 +79,11 @@ public static void ReadSemiPersistent(byte[] data) Log.Error($"Exception reading semi-persistent data for game: {e}"); } + var worldData = reader.ReadPrefixedBytes(); + try { - Multiplayer.WorldComp.ReadSemiPersistent(new ByteReader(gameData)); + Multiplayer.WorldComp.ReadSemiPersistent(new ByteReader(worldData)); } catch (Exception e) { From 88004531c8c1a20f81998f395aa7667ecb099b12 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 4 Nov 2023 19:50:04 +0100 Subject: [PATCH 09/29] TickMapTrading -> TickMapSessions --- Source/Client/AsyncTime/AsyncTimeComp.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index d811563a..6d8c0ff8 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -114,7 +114,7 @@ public void Tick() tickListRare.Tick(); tickListLong.Tick(); - TickMapTrading(); + TickMapSessions(); storyteller.StorytellerTick(); storyWatcher.StoryWatcherTick(); @@ -140,18 +140,9 @@ public void Tick() } } - public void TickMapTrading() + public void TickMapSessions() { - var trading = Multiplayer.WorldComp.trading; - - for (int i = trading.Count - 1; i >= 0; i--) - { - var session = trading[i]; - if (session.playerNegotiator.Map != map) continue; - - if (session.ShouldCancel()) - Multiplayer.WorldComp.RemoveTradeSession(session); - } + map.MpComp().sessionManager.TickSessions(); } // These are normally called in Map.MapUpdate() and react to changes in the game state even when the game is paused (not ticking) From 54d201fc87815ab3173fbfd509a27d01195aa9cf Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 20:24:32 +0100 Subject: [PATCH 10/29] Remove the unnecessary "cleanup" of trading sessions It was causing issues, and everything works fine without it. --- Source/Client/Comp/World/MultiplayerWorldComp.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index 6c500ff9..c8e91a05 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -36,11 +36,7 @@ public void ExposeData() Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); - if (Scribe.mode == LoadSaveMode.PostLoadInit) - trading.Clear(); // Reset the list, let the sessions re-add themselves from ExposeData - should prevent any potential issues with this list having different order or elements sessionManager.ExposeSessions(); - if (Scribe.mode == LoadSaveMode.PostLoadInit && MpTradeSession.current != null && TradingWindow.drawingTrade != null) - TradingWindow.drawingTrade.selectedTab = trading.IndexOf(MpTradeSession.current); // In case order changed, set the current tab DoBackCompat(); } From e22f085a873c252c8aa512a87b1b3b18aeb74b04 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 20:26:09 +0100 Subject: [PATCH 11/29] Fix ritual session not opening the dialog when started --- Source/Client/Persistent/Rituals.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index 47e11a7b..1237f1ec 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -234,7 +234,7 @@ static bool Prefix(Window window) assignments = MpUtil.ShallowCopy(dialog.assignments, new MpRitualAssignments()) }; - comp.CreateRitualSession(data); + session = comp.CreateRitualSession(data); } if (TickPatch.currentExecutingCmdIssuedBySelf) From 31750e0f12d37660d0c889a99b74cfb26da633ad Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 20:29:03 +0100 Subject: [PATCH 12/29] Fix trading session transferables, put all drawn session changes into try/finally --- Source/Client/Persistent/Trading.cs | 27 ++-- Source/Client/Persistent/TradingUI.cs | 187 +++++++++++++------------- 2 files changed, 111 insertions(+), 103 deletions(-) diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 271b0c22..b5c0a2e1 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -72,11 +72,11 @@ public static MpTradeSession TryCreate(ITrader trader, Pawn playerNegotiator, bo if (!Multiplayer.WorldComp.sessionManager.AddSession(session)) return null; - CancelTradeDealReset.cancel = true; - SetTradeSession(session); - try { + CancelTradeDealReset.cancel = true; + SetTradeSession(session); + session.deal = new MpTradeDeal(session); Thing permSilver = ThingMaker.MakeThing(ThingDefOf.Silver, null); @@ -129,14 +129,22 @@ public bool ShouldCancel() [SyncMethod] public void TryExecute() { - SetTradeSession(this); + bool executed = false; + + try + { + SetTradeSession(this); - deal.recacheColony = true; - deal.recacheTrader = true; - deal.Recache(); + deal.recacheColony = true; + deal.recacheTrader = true; + deal.Recache(); - bool executed = deal.TryExecute(out bool traded); - SetTradeSession(null); + executed = deal.TryExecute(out bool traded); + } + finally + { + SetTradeSession(null); + } if (executed) Multiplayer.WorldComp.RemoveTradeSession(this); @@ -159,6 +167,7 @@ public void ToggleGiftMode() public static void SetTradeSession(MpTradeSession session) { + SyncSessionWithTransferablesMarker.DrawnThingFilter = session; current = session; TradeSession.trader = session?.trader; TradeSession.playerNegotiator = session?.playerNegotiator; diff --git a/Source/Client/Persistent/TradingUI.cs b/Source/Client/Persistent/TradingUI.cs index 75b86969..422326d5 100644 --- a/Source/Client/Persistent/TradingUI.cs +++ b/Source/Client/Persistent/TradingUI.cs @@ -35,102 +35,96 @@ public TradingWindow() public override void DoWindowContents(Rect inRect) { - SyncSessionWithTransferablesMarker.DrawnThingFilter = MpTradeSession.current; + added.RemoveAll(kv => Time.time - kv.Value > 1f); + removed.RemoveAll(kv => Time.time - kv.Value > 0.5f && RemoveCachedTradeable(kv.Key)); + + tabs.Clear(); + var trading = Multiplayer.WorldComp.trading; + for (int i = 0; i < trading.Count; i++) + { + int j = i; + tabs.Add(new TabRecord(trading[i].Label, () => selectedTab = j, () => selectedTab == j)); + } + + if (selectedTab == -1 && trading.Count > 0) + selectedTab = 0; + + if (selectedTab == -1) + { + Close(); + return; + } + + int rows = Mathf.CeilToInt(tabs.Count / 3f); + inRect.yMin += rows * TabDrawer.TabHeight + 3; + TabDrawer.DrawTabs(inRect, tabs, rows); + + inRect.yMin += 10f; + + var session = Multiplayer.WorldComp.trading[selectedTab]; + if (session.SessionId != selectedSession) + { + RecreateDialog(); + selectedSession = session.SessionId; + } try { - added.RemoveAll(kv => Time.time - kv.Value > 1f); - removed.RemoveAll(kv => Time.time - kv.Value > 0.5f && RemoveCachedTradeable(kv.Key)); + MpTradeSession.SetTradeSession(session); + drawingTrade = this; - tabs.Clear(); - var trading = Multiplayer.WorldComp.trading; - for (int i = 0; i < trading.Count; i++) + if (session.deal.ShouldRecache) + session.deal.Recache(); + + if (session.deal.uiShouldReset != UIShouldReset.None) { - int j = i; - tabs.Add(new TabRecord(trading[i].Label, () => selectedTab = j, () => selectedTab == j)); - } + if (session.deal.uiShouldReset != UIShouldReset.Silent) + BeforeCache(); - if (selectedTab == -1 && trading.Count > 0) - selectedTab = 0; + dialog.CacheTradeables(); + dialog.CountToTransferChanged(); - if (selectedTab == -1) - { - Close(); - return; + session.deal.uiShouldReset = UIShouldReset.None; } - int rows = Mathf.CeilToInt(tabs.Count / 3f); - inRect.yMin += rows * TabDrawer.TabHeight + 3; - TabDrawer.DrawTabs(inRect, tabs, rows); + if (session.deal.caravanDirty) + { + dialog.CountToTransferChanged(); + session.deal.caravanDirty = false; + } - inRect.yMin += 10f; + GUI.BeginGroup(inRect); + { + Rect groupRect = new Rect(0, 0, inRect.width, inRect.height); + dialog.DoWindowContents(groupRect); + } + GUI.EndGroup(); - var session = Multiplayer.WorldComp.trading[selectedTab]; - if (session.SessionId != selectedSession) + int? traderLeavingIn = GetTraderTime(TradeSession.trader); + if (traderLeavingIn != null) { - RecreateDialog(); - selectedSession = session.SessionId; + float num = inRect.width - 590f; + Rect position = new Rect(inRect.x + num, inRect.y, inRect.width - num, 58f); + Rect traderNameRect = new Rect(position.x + position.width / 2f, position.y, position.width / 2f - 1f, position.height); + Rect traderTimeRect = traderNameRect.Up(traderNameRect.height - 5f); + + Text.Anchor = TextAnchor.LowerRight; + Widgets.Label(traderTimeRect, "MpTraderLeavesIn".Translate(traderLeavingIn?.ToStringTicksToPeriod())); + Text.Anchor = TextAnchor.UpperLeft; } + if (cancelPressed) { - MpTradeSession.SetTradeSession(session); - drawingTrade = this; - - if (session.deal.ShouldRecache) - session.deal.Recache(); - - if (session.deal.uiShouldReset != UIShouldReset.None) - { - if (session.deal.uiShouldReset != UIShouldReset.Silent) - BeforeCache(); - - dialog.CacheTradeables(); - dialog.CountToTransferChanged(); - - session.deal.uiShouldReset = UIShouldReset.None; - } - - if (session.deal.caravanDirty) - { - dialog.CountToTransferChanged(); - session.deal.caravanDirty = false; - } - - GUI.BeginGroup(inRect); - { - Rect groupRect = new Rect(0, 0, inRect.width, inRect.height); - dialog.DoWindowContents(groupRect); - } - GUI.EndGroup(); - - int? traderLeavingIn = GetTraderTime(TradeSession.trader); - if (traderLeavingIn != null) - { - float num = inRect.width - 590f; - Rect position = new Rect(inRect.x + num, inRect.y, inRect.width - num, 58f); - Rect traderNameRect = new Rect(position.x + position.width / 2f, position.y, position.width / 2f - 1f, position.height); - Rect traderTimeRect = traderNameRect.Up(traderNameRect.height - 5f); - - Text.Anchor = TextAnchor.LowerRight; - Widgets.Label(traderTimeRect, "MpTraderLeavesIn".Translate(traderLeavingIn?.ToStringTicksToPeriod())); - Text.Anchor = TextAnchor.UpperLeft; - } - - if (cancelPressed) - { - CancelTradeSession(session); - cancelPressed = false; - } - - session.giftMode = TradeSession.giftMode; - - drawingTrade = null; - MpTradeSession.SetTradeSession(null); + CancelTradeSession(session); + cancelPressed = false; } + + session.giftMode = TradeSession.giftMode; } finally { - SyncSessionWithTransferablesMarker.DrawnThingFilter = null; + drawingTrade = null; + MpTradeSession.SetTradeSession(null); } } @@ -162,20 +156,25 @@ private void RecreateDialog() { var session = Multiplayer.WorldComp.trading[selectedTab]; - MpTradeSession.SetTradeSession(session); - - dialog = MpUtil.NewObjectNoCtor(); - dialog.quickSearchWidget = new QuickSearchWidget(); - dialog.giftsOnly = session.giftsOnly; - dialog.sorter1 = TransferableSorterDefOf.Category; - dialog.sorter2 = TransferableSorterDefOf.MarketValue; - dialog.CacheTradeables(); - session.deal.uiShouldReset = UIShouldReset.None; - - removed.Clear(); - added.Clear(); - - MpTradeSession.SetTradeSession(null); + try + { + MpTradeSession.SetTradeSession(session); + + dialog = MpUtil.NewObjectNoCtor(); + dialog.quickSearchWidget = new QuickSearchWidget(); + dialog.giftsOnly = session.giftsOnly; + dialog.sorter1 = TransferableSorterDefOf.Category; + dialog.sorter2 = TransferableSorterDefOf.MarketValue; + dialog.CacheTradeables(); + session.deal.uiShouldReset = UIShouldReset.None; + + removed.Clear(); + added.Clear(); + } + finally + { + MpTradeSession.SetTradeSession(null); + } } public void Notify_RemovedSession(int index) @@ -405,7 +404,7 @@ static void Prefix(ref bool __state) } } - static void Postfix(bool __state) + static void Finalizer(bool __state) { if (__state) MpTradeSession.SetTradeSession(null); @@ -429,7 +428,7 @@ static bool Prefix(ref bool __state, ref string __result) return true; } - static void Postfix(bool __state) + static void Finalizer(bool __state) { if (__state) MpTradeSession.SetTradeSession(null); @@ -484,7 +483,7 @@ static void Prefix(ref bool __state) } } - static void Postfix(bool __state) + static void Finalizer(bool __state) { if (__state) { From 21cc44969c98f2886de3e9d3523fb2a616d02788 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 20:30:23 +0100 Subject: [PATCH 13/29] Rename `DrawnThingFilter` to `DrawnSessionWithTransferables` --- Source/Client/Persistent/CaravanFormingProxy.cs | 4 ++-- Source/Client/Persistent/CaravanSplittingProxy.cs | 4 ++-- Source/Client/Persistent/Trading.cs | 2 +- Source/Client/Persistent/TransporterLoadingProxy.cs | 4 ++-- Source/Client/Syncing/Game/SyncFields.cs | 2 +- .../Syncing/Game/SyncSessionWithTransferablesMarker.cs | 10 +++++----- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/Client/Persistent/CaravanFormingProxy.cs b/Source/Client/Persistent/CaravanFormingProxy.cs index 1e2068ca..c8886833 100644 --- a/Source/Client/Persistent/CaravanFormingProxy.cs +++ b/Source/Client/Persistent/CaravanFormingProxy.cs @@ -18,7 +18,7 @@ public CaravanFormingProxy(Map map, bool reform = false, Action onClosed = null, public override void DoWindowContents(Rect inRect) { var session = Session; - SyncSessionWithTransferablesMarker.DrawnThingFilter = session; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = session; drawing = this; try @@ -44,7 +44,7 @@ public override void DoWindowContents(Rect inRect) finally { drawing = null; - SyncSessionWithTransferablesMarker.DrawnThingFilter = null; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = null; } } } diff --git a/Source/Client/Persistent/CaravanSplittingProxy.cs b/Source/Client/Persistent/CaravanSplittingProxy.cs index d8c462d9..41ac91c9 100644 --- a/Source/Client/Persistent/CaravanSplittingProxy.cs +++ b/Source/Client/Persistent/CaravanSplittingProxy.cs @@ -44,7 +44,7 @@ public override void PostOpen() /// public override void DoWindowContents(Rect inRect) { - SyncSessionWithTransferablesMarker.DrawnThingFilter = session; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = session; try { @@ -109,7 +109,7 @@ public override void DoWindowContents(Rect inRect) } finally { - SyncSessionWithTransferablesMarker.DrawnThingFilter = null; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = null; } } diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index b5c0a2e1..cd1a277d 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -167,7 +167,7 @@ public void ToggleGiftMode() public static void SetTradeSession(MpTradeSession session) { - SyncSessionWithTransferablesMarker.DrawnThingFilter = session; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = session; current = session; TradeSession.trader = session?.trader; TradeSession.playerNegotiator = session?.playerNegotiator; diff --git a/Source/Client/Persistent/TransporterLoadingProxy.cs b/Source/Client/Persistent/TransporterLoadingProxy.cs index e493c86e..a5a5e0f3 100644 --- a/Source/Client/Persistent/TransporterLoadingProxy.cs +++ b/Source/Client/Persistent/TransporterLoadingProxy.cs @@ -20,7 +20,7 @@ public TransporterLoadingProxy(Map map, List transporters) : ba public override void DoWindowContents(Rect inRect) { var session = Session; - SyncSessionWithTransferablesMarker.DrawnThingFilter = session; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = session; drawing = this; try @@ -40,7 +40,7 @@ public override void DoWindowContents(Rect inRect) finally { drawing = null; - SyncSessionWithTransferablesMarker.DrawnThingFilter = null; + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = null; } } } diff --git a/Source/Client/Syncing/Game/SyncFields.cs b/Source/Client/Syncing/Game/SyncFields.cs index 091973cc..3e7616b5 100644 --- a/Source/Client/Syncing/Game/SyncFields.cs +++ b/Source/Client/Syncing/Game/SyncFields.cs @@ -449,7 +449,7 @@ static void RenameZone(Dialog_RenameZone __instance) [MpPrefix(typeof(TransferableUIUtility), "DoCountAdjustInterface")] static void TransferableAdjustTo(Transferable trad) { - var session = SyncSessionWithTransferablesMarker.DrawnThingFilter; + var session = SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables; if (session != null) SyncTradeableCount.Watch(new MpTransferableReference(session, trad)); } diff --git a/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs b/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs index b0957e2f..a9118e37 100644 --- a/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs +++ b/Source/Client/Syncing/Game/SyncSessionWithTransferablesMarker.cs @@ -5,17 +5,17 @@ namespace Multiplayer.Client; public class SyncSessionWithTransferablesMarker { - private static ISessionWithTransferables thingFilterContext; + private static ISessionWithTransferables drawnSessionWithTransferables; - public static ISessionWithTransferables DrawnThingFilter + public static ISessionWithTransferables DrawnSessionWithTransferables { - get => thingFilterContext; + get => drawnSessionWithTransferables; set { - if (value != null && thingFilterContext != null) + if (value != null && drawnSessionWithTransferables != null) throw new Exception("Session with transferables context already set!"); - thingFilterContext = value; + drawnSessionWithTransferables = value; } } } From 5acf4fe174d63ba7d0599cdac6ee30aa96d3cf56 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 20:45:54 +0100 Subject: [PATCH 14/29] Remove unnecessary commented-out code --- Source/Client/AsyncTime/AsyncTimeComp.cs | 7 ------- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 2 -- 2 files changed, 9 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 6d8c0ff8..b3e3cfc0 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -25,13 +25,6 @@ public float TickRateMultiplier(TimeSpeed speed) var enforcePause = comp.sessionManager.IsAnySessionCurrentlyPausing(map) || Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(map); - // var enforcePause = comp.transporterLoading != null || - // comp.caravanForming != null || - // comp.ritualSession != null || - // comp.mapDialogs.Any() || - // Multiplayer.WorldComp.AnyTradeSessionsOnMap(map) || - // Multiplayer.WorldComp.splitSession != null || - // pauseLocks.Any(x => x(map)); if (enforcePause) return 0f; diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index a576d3f7..0fefc8be 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -28,8 +28,6 @@ public float TickRateMultiplier(TimeSpeed speed) if (Multiplayer.GameComp.asyncTime) { var enforcePause = Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(null); - // var enforcePause = Multiplayer.WorldComp.splitSession != null || - // AsyncTimeComp.pauseLocks.Any(x => x(null)); if (enforcePause) return 0f; From 6b660abb51afb3a68864f1e0766f761ea93ca9af Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 21:12:24 +0100 Subject: [PATCH 15/29] Call `ISession:PostRemoveSession` from `SessionManager:ExposeSessions` cleanup --- Source/Client/Persistent/SessionManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index 4e37aae1..615073ff 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -248,6 +248,7 @@ public void ExposeSessions() { // Removal from allSessions handled lower exposableSessions.RemoveAt(i); + session.PostRemoveSession(); var sessionType = session.GetType(); if (!tempCleanupLoggingTypes.Add(sessionType)) Log.Message($"Multiplayer session not valid after exposing data: {sessionType}"); From c2c11d997d1306eae2072d583f6044f457d93b1a Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sun, 5 Nov 2023 21:19:04 +0100 Subject: [PATCH 16/29] Rename WindowSession back to Session --- Source/Client/Experimental/{WindowSession.cs => Session.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Source/Client/Experimental/{WindowSession.cs => Session.cs} (100%) diff --git a/Source/Client/Experimental/WindowSession.cs b/Source/Client/Experimental/Session.cs similarity index 100% rename from Source/Client/Experimental/WindowSession.cs rename to Source/Client/Experimental/Session.cs From 237678d942369b71669b249b691c1374aa3e0061 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 15:09:30 +0100 Subject: [PATCH 17/29] Make session manager sync semi persistent session ID Also error for semi persistent data sync now says writing semi persistent instead of exposing --- Source/Client/Persistent/SessionManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index 615073ff..7a23354c 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -160,7 +160,7 @@ public void WriteSemiPersistent(ByteWriter data) allSessions.Remove(session); var sessionType = session.GetType(); if (!tempCleanupLoggingTypes.Add(sessionType)) - Log.Message($"Multiplayer session not valid after exposing data: {sessionType}"); + Log.Message($"Multiplayer session not valid after writing semi persistent data: {sessionType}"); } } // Clear the set again to not leave behind any unneeded stuff @@ -172,6 +172,7 @@ public void WriteSemiPersistent(ByteWriter data) { data.WriteUShort((ushort)ImplSerialization.sessions.FindIndex(session.GetType())); data.WriteInt32(session.Map?.uniqueID ?? -1); + data.WriteInt32(session.SessionId); try { @@ -194,6 +195,7 @@ public void ReadSemiPersistent(ByteReader data) { ushort typeIndex = data.ReadUShort(); int mapId = data.ReadInt32(); + int sessionId = data.ReadInt32(); if (typeIndex >= ImplSerialization.sessions.Length) { @@ -217,6 +219,7 @@ public void ReadSemiPersistent(ByteReader data) { if (Activator.CreateInstance(objType, map) is ISemiPersistentSession session) { + session.SessionId = sessionId; session.Read(new ReadingSyncWorker(data)); semiPersistentSessions.Add(session); allSessions.Add(session); From 4f7f2ad6e9b80d0b8550f678b273dc11c6181f06 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 19:37:00 +0100 Subject: [PATCH 18/29] Stop sessions from self-assigning IDs, let session manager handle it --- Source/Client/Persistent/CaravanSplittingSession.cs | 1 - Source/Client/Persistent/Rituals.cs | 2 -- Source/Client/Persistent/Trading.cs | 2 -- Source/Client/Persistent/TransporterLoadingSession.cs | 1 - 4 files changed, 6 deletions(-) diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index 106e1980..28359370 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -42,7 +42,6 @@ public class CaravanSplittingSession : Session, IExposableSession, ISessionWithT /// public CaravanSplittingSession(Caravan caravan) { - sessionId = Find.UniqueIDsManager.GetNextThingID(); Caravan = caravan; AddItems(); diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index 1237f1ec..ce23f5c5 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -27,8 +27,6 @@ public RitualSession(Map map) public RitualSession(Map map, RitualData data) { - SessionId = Find.UniqueIDsManager.GetNextThingID(); - this.map = map; this.data = data; this.data.assignments.session = this; diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index cd1a277d..cbcc77da 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -42,8 +42,6 @@ public MpTradeSession() { } private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) { - sessionId = Find.UniqueIDsManager.GetNextThingID(); - this.trader = trader; this.playerNegotiator = playerNegotiator; this.giftMode = giftMode; diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index f208ccba..329ad2e1 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -27,7 +27,6 @@ public TransporterLoading(Map map) public TransporterLoading(Map map, List transporters) : this(map) { - sessionId = Find.UniqueIDsManager.GetNextThingID(); this.transporters = transporters; pods = transporters.Select(t => t.parent).ToList(); From 5320d3f0d4c088a243b016a583aafa4d05f881ec Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 19:38:13 +0100 Subject: [PATCH 19/29] Added XML documentation to most session interfaces --- Source/Client/Persistent/ISession.cs | 84 ++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/Source/Client/Persistent/ISession.cs b/Source/Client/Persistent/ISession.cs index 0993da40..af33971f 100644 --- a/Source/Client/Persistent/ISession.cs +++ b/Source/Client/Persistent/ISession.cs @@ -1,28 +1,59 @@ using Multiplayer.API; +using Multiplayer.Client.Experimental; using RimWorld; using Verse; namespace Multiplayer.Client.Persistent { + /// + /// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. + /// Sessions will be reset/reloaded during reloading - to prevent it, implement or . + /// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. + /// public interface ISession { + /// + /// The map this session is used by or in case of global sessions. + /// Map Map { get; } + /// + /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. + /// Automatically applied by the session manager + /// If inheriting you don't have to worry about this property. + /// int SessionId { get; set; } + /// + /// Used by the session manager while joining the game - if it returns it'll get removed. + /// bool IsSessionValid { get; } /// - /// + /// Called when checking ticking and if any session returns - it'll force pause the map/game. + /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. /// /// Current map (when checked from local session manager) or (when checked from local session manager). - /// + /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . + /// if the session should pause the map/game, otherwise. bool IsCurrentlyPausing(Map map); + /// + /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. + /// + /// Currently processed colonist bar entry. Will be called once per . + /// Menu option that will be displayed when the session is active. Can be . FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); + /// + /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. + /// + /// In case of , this will only be called if successfully added. void PostAddSession(); + /// + /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. + /// void PostRemoveSession(); } @@ -32,17 +63,51 @@ public interface IPausingWithDialog void OpenWindow(bool sound = true); } + /// + /// Required by sessions dealing with transferables, like trading or caravan forming. By implementing this interface, Multiplayer will handle majority of syncing of changes in transferables. + /// When drawing the dialog tied to this session, you'll have to set [REF TO SETTER/METHOD] to the proper session, and set it to null once done. + /// + /// For safety, make sure to set [REF TO SETTER/METHOD] in and unset in . + /// TODO: Replace [REF TO SETTER/METHOD] with actual ref in API public interface ISessionWithTransferables : ISession { + /// + /// Used when syncing data across players, specifically to retrieve based on the it has. + /// + /// of the . + /// which corresponds to a with specific . Transferable GetTransferableByThingId(int thingId); + /// + /// Called when the count in a specific was changed. + /// + /// Transferable whose count was changed. void Notify_CountChanged(Transferable tr); } + /// + /// Sessions implementing this interface consist of persistent data. + /// When inheriting from , remember to call base.ExposeData() to let it handle + /// Persistent data: + /// + /// Serialized into XML using RimWorld's Scribe system + /// Save-bound: survives a server restart + /// + /// + /// A class should NOT implement both this and - it'll be treated as if only implementing this interface. public interface IExposableSession : ISession, IExposable { } + /// + /// Sessions implementing this interface consist of semi-persistent data. + /// Semi-persistent data: + /// + /// Serialized into binary using the Sync system + /// Session-bound: survives a reload, lost when the server is closed + /// + /// + /// A class should NOT implement both this and - it'll be treated as if only implementing . public interface ISemiPersistentSession : ISession { void Write(SyncWorker sync); @@ -50,19 +115,30 @@ public interface ISemiPersistentSession : ISession void Read(SyncWorker sync); } + /// + /// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type. + /// public interface ISessionWithCreationRestrictions { /// - /// Method used to check if the current session can be created by checking every other existing . - /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. + /// Method used to check if the current session can be created by checking other . + /// Only sessions in the current context are checked (local map sessions or global sessions). /// /// The other session the current one is checked against. Can be of different type. + /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. /// if the current session should be created, otherwise bool CanExistWith(ISession other); } + /// + /// Used by sessions that are are required to tick together with the map/world. + /// public interface ITickingSession { + /// + /// Called once per session when the map (for local sessions) or the world (for global sessions) is ticking. + /// + /// The sessions are iterated over backwards using a for loop, so it's safe for them to remove themselves from the session manager. void Tick(); } } From a3438f880f1d26336cc52636aed270438aa43ba1 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 19:39:21 +0100 Subject: [PATCH 20/29] ISemiPersistentSession Write/Read -> Sync, make rituals use it --- Source/Client/Persistent/ISession.cs | 8 ++++--- Source/Client/Persistent/Rituals.cs | 25 ++++++++++------------ Source/Client/Persistent/SessionManager.cs | 4 ++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Source/Client/Persistent/ISession.cs b/Source/Client/Persistent/ISession.cs index af33971f..18c31ec6 100644 --- a/Source/Client/Persistent/ISession.cs +++ b/Source/Client/Persistent/ISession.cs @@ -110,9 +110,11 @@ public interface IExposableSession : ISession, IExposable /// A class should NOT implement both this and - it'll be treated as if only implementing . public interface ISemiPersistentSession : ISession { - void Write(SyncWorker sync); - - void Read(SyncWorker sync); + /// + /// Writes/reads the data used by this session. + /// + /// Sync worker used for writing/reading the data. + void Sync(SyncWorker sync); } /// diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index ce23f5c5..02fdfa82 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -1,6 +1,5 @@ using HarmonyLib; using Multiplayer.API; -using Multiplayer.Common; using RimWorld; using System; using System.Collections.Generic; @@ -13,7 +12,7 @@ namespace Multiplayer.Client.Persistent { - public class RitualSession : Session, IPausingWithDialog + public class RitualSession : Session, IPausingWithDialog, ISemiPersistentSession { public Map map; public RitualData data; @@ -75,19 +74,17 @@ public void OpenWindow(bool sound = true) Find.WindowStack.Add(dialog); } - public void Write(ByteWriter writer) + public void Sync(SyncWorker sync) { - writer.WriteInt32(SessionId); - writer.MpContext().map = map; - - SyncSerialization.WriteSync(writer, data); - } - - public void Read(ByteReader reader) - { - SessionId = reader.ReadInt32(); - data = SyncSerialization.ReadSync(reader); - data.assignments.session = this; + if (sync.isWriting) + { + sync.Write(data); + } + else + { + data = sync.Read(); + data.assignments.session = this; + } } public override bool IsCurrentlyPausing(Map map) => map == this.map; diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index 7a23354c..1c442d46 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -176,7 +176,7 @@ public void WriteSemiPersistent(ByteWriter data) try { - session.Write(new WritingSyncWorker(data)); + session.Sync(new WritingSyncWorker(data)); } catch (Exception e) { @@ -220,7 +220,7 @@ public void ReadSemiPersistent(ByteReader data) if (Activator.CreateInstance(objType, map) is ISemiPersistentSession session) { session.SessionId = sessionId; - session.Read(new ReadingSyncWorker(data)); + session.Sync(new ReadingSyncWorker(data)); semiPersistentSessions.Add(session); allSessions.Add(session); } From d1d5810c3b56d69f6ce4dc1f7a6f3ee79cafb086 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 19:52:16 +0100 Subject: [PATCH 21/29] Slightly change how pause lock session works --- Source/Client/Comp/World/MultiplayerWorldComp.cs | 3 +++ Source/Client/Persistent/PauseLockSession.cs | 6 ++++-- Source/Client/Syncing/Sync.cs | 11 +++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index c8e91a05..2e32d56c 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -37,6 +37,9 @@ public void ExposeData() Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); sessionManager.ExposeSessions(); + // Ensure a pause lock session exists if there's any pause locks registered + if (!PauseLockSession.pauseLocks.NullOrEmpty()) + sessionManager.AddSession(new PauseLockSession()); DoBackCompat(); } diff --git a/Source/Client/Persistent/PauseLockSession.cs b/Source/Client/Persistent/PauseLockSession.cs index 0f85cc33..9a41158a 100644 --- a/Source/Client/Persistent/PauseLockSession.cs +++ b/Source/Client/Persistent/PauseLockSession.cs @@ -8,9 +8,9 @@ namespace Multiplayer.Client.Persistent; // Used for pause locks. Pause locks should become obsolete and this should become unused, // but pause locks are kept for backwards compatibility. -public class PauseLockSession : Session +public class PauseLockSession : Session, ISessionWithCreationRestrictions { - public List pauseLocks = new(); + public static List pauseLocks = new(); public override Map Map => null; @@ -18,4 +18,6 @@ public class PauseLockSession : Session // Should we add some message explaining pause locks/having a list of pausing ones? public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) => null; + + public bool CanExistWith(ISession other) => other is not PauseLockSession; } diff --git a/Source/Client/Syncing/Sync.cs b/Source/Client/Syncing/Sync.cs index 4dc41823..f6a1c631 100644 --- a/Source/Client/Syncing/Sync.cs +++ b/Source/Client/Syncing/Sync.cs @@ -381,15 +381,10 @@ public static void RegisterPauseLock(MethodInfo method) public static void RegisterPauseLock(PauseLockDelegate pauseLock) { - var session = Multiplayer.WorldComp.sessionManager.GetFirstOfType(); - if (session == null) - { - // Only ever add pause lock session if we have any pause locks - session = new PauseLockSession(); - Multiplayer.WorldComp.sessionManager.AddSession(session); - } + if (PauseLockSession.pauseLocks.Contains(pauseLock)) + throw new Exception($"Pause lock already registered: {pauseLock}"); - session.pauseLocks.Add(pauseLock); + PauseLockSession.pauseLocks.Add(pauseLock); } public static void ValidateAll() From ceb1a838f89ecb2bb173e109c83dd77a6bb482f6 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 19:59:17 +0100 Subject: [PATCH 22/29] Added ISessionManagerAPI The intention is to move it to MP API as a way to provide a way for other mods to interact with the session manager. Currently needs a little bit more documentation (currently all documentation was moved from SessionManager). --- .../Client/Experimental/ISessionManagerAPI.cs | 50 +++++++++++++++++++ Source/Client/Persistent/SessionManager.cs | 23 +-------- 2 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 Source/Client/Experimental/ISessionManagerAPI.cs diff --git a/Source/Client/Experimental/ISessionManagerAPI.cs b/Source/Client/Experimental/ISessionManagerAPI.cs new file mode 100644 index 00000000..22c3ff98 --- /dev/null +++ b/Source/Client/Experimental/ISessionManagerAPI.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Multiplayer.Client.Persistent; +using Verse; + +namespace Multiplayer.Client.Experimental; + +public interface ISessionManagerAPI +{ + IReadOnlyList AllSessions { get; } + IReadOnlyList ExposableSessions { get; } + IReadOnlyList SemiPersistentSessions { get; } + IReadOnlyList TickingSessions { get; } + bool AnySessionActive { get; } + + /// + /// Adds a new session to the list of active sessions. + /// + /// The session to try to add to active sessions. + /// if the session was added to active ones, if there was a conflict between sessions. + bool AddSession(ISession session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. + ISession GetOrAddSessionAnyConflict(ISession session); + + /// + /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . + /// + /// The session to try to add to active sessions. + /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. + T GetOrAddSession(T session) where T : ISession; + + /// + /// Tries to remove a session from active ones. + /// + /// The session to try to remove from the active sessions. + /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. + bool RemoveSession(ISession session); + + T GetFirstOfType() where T : ISession; + + T GetFirstWithId(int id) where T : ISession; + + ISession GetFirstWithId(int id); + + bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API? +} diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index 1c442d46..f23748a6 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Saving; using Multiplayer.Common; using RimWorld; @@ -9,7 +10,7 @@ namespace Multiplayer.Client.Persistent; -public class SessionManager : IHasSemiPersistentData +public class SessionManager : IHasSemiPersistentData, ISessionManagerAPI { public IReadOnlyList AllSessions => allSessions.AsReadOnly(); public IReadOnlyList ExposableSessions => exposableSessions.AsReadOnly(); @@ -24,11 +25,6 @@ public class SessionManager : IHasSemiPersistentData public bool AnySessionActive => allSessions.Count > 0; - /// - /// Adds a new session to the list of active sessions. - /// - /// The session to try to add to active sessions. - /// if the session was added to active ones, if there was a conflict between sessions. public bool AddSession(ISession session) { if (GetFirstConflictingSession(session) != null) @@ -38,11 +34,6 @@ public bool AddSession(ISession session) return true; } - /// - /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . - /// - /// The session to try to add to active sessions. - /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. public ISession GetOrAddSessionAnyConflict(ISession session) { if (GetFirstConflictingSession(session) is { } other) @@ -52,11 +43,6 @@ public ISession GetOrAddSessionAnyConflict(ISession session) return session; } - /// - /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . - /// - /// The session to try to add to active sessions. - /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. public T GetOrAddSession(T session) where T : ISession { if (session is ISessionWithCreationRestrictions s) @@ -98,11 +84,6 @@ private void AddSessionNoCheck(ISession session) session.PostAddSession(); } - /// - /// Tries to remove a session from active ones. - /// - /// The session to try to remove from the active sessions. - /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. public bool RemoveSession(ISession session) { if (session is IExposableSession exposable) From d12a6cf651b1c1926a5479c7ab8b8bb975365e7f Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Mon, 6 Nov 2023 20:23:07 +0100 Subject: [PATCH 23/29] Expose `MultiplayerGameComp.nextSessionId` --- Source/Client/Comp/Game/MultiplayerGameComp.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Client/Comp/Game/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs index 47c7cc3f..d7e14a69 100644 --- a/Source/Client/Comp/Game/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -33,6 +33,7 @@ public void ExposeData() Scribe_Values.Look(ref logDesyncTraces, "logDesyncTraces"); Scribe_Values.Look(ref pauseOnLetter, "pauseOnLetter"); Scribe_Values.Look(ref timeControl, "timeControl"); + Scribe_Values.Look(ref nextSessionId, "nextSessionId"); // Store for back-compat conversion in GameExposeComponentsPatch if (Scribe.mode == LoadSaveMode.LoadingVars) From bc5d2e2173a153dcf80908ddd023e303adb0df2e Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Fri, 10 Nov 2023 18:00:35 +0100 Subject: [PATCH 24/29] Rename `ISessionManagerAPI` to `ISessionManager` --- Source/Client/Experimental/ISessionManagerAPI.cs | 2 +- Source/Client/Persistent/SessionManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Client/Experimental/ISessionManagerAPI.cs b/Source/Client/Experimental/ISessionManagerAPI.cs index 22c3ff98..457614d5 100644 --- a/Source/Client/Experimental/ISessionManagerAPI.cs +++ b/Source/Client/Experimental/ISessionManagerAPI.cs @@ -4,7 +4,7 @@ namespace Multiplayer.Client.Experimental; -public interface ISessionManagerAPI +public interface ISessionManager { IReadOnlyList AllSessions { get; } IReadOnlyList ExposableSessions { get; } diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index f23748a6..f76ff7a9 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -10,7 +10,7 @@ namespace Multiplayer.Client.Persistent; -public class SessionManager : IHasSemiPersistentData, ISessionManagerAPI +public class SessionManager : IHasSemiPersistentData, ISessionManager { public IReadOnlyList AllSessions => allSessions.AsReadOnly(); public IReadOnlyList ExposableSessions => exposableSessions.AsReadOnly(); From 78bee79f9794e7190d7279424bc79e7542e83658 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Fri, 10 Nov 2023 18:16:57 +0100 Subject: [PATCH 25/29] Rename ISessionManager file --- .../Experimental/{ISessionManagerAPI.cs => ISessionManager.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Source/Client/Experimental/{ISessionManagerAPI.cs => ISessionManager.cs} (100%) diff --git a/Source/Client/Experimental/ISessionManagerAPI.cs b/Source/Client/Experimental/ISessionManager.cs similarity index 100% rename from Source/Client/Experimental/ISessionManagerAPI.cs rename to Source/Client/Experimental/ISessionManager.cs From e238082fe93045f985895874ceff5ac09283b0d0 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Fri, 10 Nov 2023 18:24:28 +0100 Subject: [PATCH 26/29] Remove 3 session interfaces in favor of abstract classes Removed `ISession`, `IExposableSession`, and `ISemiPersistentSession` in favor of abstract classes `Session`, `ExposableSession`, and `SemiPersistentSession`. There was likely no class besides `Session` that would implement `ISession`, so it was removed. The other 2 were removed in favor of turning them into abstract subclasses of `Session`, which will allow for expanding them further in the future, and prevent people from implementing both interfaces (which was not supported). All the xml docs were moved from interfaces to their respective classes. --- Source/Client/Experimental/ISessionManager.cs | 20 ++--- Source/Client/Experimental/Session.cs | 85 +++++++++++++++--- Source/Client/MultiplayerGame.cs | 4 +- .../Persistent/CaravanFormingSession.cs | 4 +- .../Persistent/CaravanSplittingSession.cs | 4 +- Source/Client/Persistent/ISession.cs | 88 +------------------ Source/Client/Persistent/PauseLockSession.cs | 2 +- Source/Client/Persistent/Rituals.cs | 4 +- Source/Client/Persistent/SessionManager.cs | 62 +++++-------- Source/Client/Persistent/Trading.cs | 4 +- .../Persistent/TransporterLoadingSession.cs | 2 +- .../Syncing/Dict/SyncDictMultiplayer.cs | 3 +- Source/Client/Syncing/ImplSerialization.cs | 2 +- 13 files changed, 124 insertions(+), 160 deletions(-) diff --git a/Source/Client/Experimental/ISessionManager.cs b/Source/Client/Experimental/ISessionManager.cs index 457614d5..6a515926 100644 --- a/Source/Client/Experimental/ISessionManager.cs +++ b/Source/Client/Experimental/ISessionManager.cs @@ -6,9 +6,9 @@ namespace Multiplayer.Client.Experimental; public interface ISessionManager { - IReadOnlyList AllSessions { get; } - IReadOnlyList ExposableSessions { get; } - IReadOnlyList SemiPersistentSessions { get; } + IReadOnlyList AllSessions { get; } + IReadOnlyList ExposableSessions { get; } + IReadOnlyList SemiPersistentSessions { get; } IReadOnlyList TickingSessions { get; } bool AnySessionActive { get; } @@ -17,34 +17,34 @@ public interface ISessionManager /// /// The session to try to add to active sessions. /// if the session was added to active ones, if there was a conflict between sessions. - bool AddSession(ISession session); + bool AddSession(Session session); /// /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . /// /// The session to try to add to active sessions. /// A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input. - ISession GetOrAddSessionAnyConflict(ISession session); + Session GetOrAddSessionAnyConflict(Session session); /// /// Tries to get a conflicting session (through the use of ) or, if there was none, returns the input . /// /// The session to try to add to active sessions. /// A session that was conflicting with the input one if it's the same type (other is T), null if it's a different type, or the input itself if there were no conflicts. - T GetOrAddSession(T session) where T : ISession; + T GetOrAddSession(T session) where T : Session; /// /// Tries to remove a session from active ones. /// /// The session to try to remove from the active sessions. /// if successfully removed from . Doesn't correspond to if it was successfully removed from other lists of sessions. - bool RemoveSession(ISession session); + bool RemoveSession(Session session); - T GetFirstOfType() where T : ISession; + T GetFirstOfType() where T : Session; - T GetFirstWithId(int id) where T : ISession; + T GetFirstWithId(int id) where T : Session; - ISession GetFirstWithId(int id); + Session GetFirstWithId(int id); bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API? } diff --git a/Source/Client/Experimental/Session.cs b/Source/Client/Experimental/Session.cs index 29d9c8cd..a44ce9e3 100644 --- a/Source/Client/Experimental/Session.cs +++ b/Source/Client/Experimental/Session.cs @@ -1,35 +1,48 @@ -using Multiplayer.Client.Persistent; +using Multiplayer.API; +using Multiplayer.Client.Persistent; using RimWorld; using RimWorld.Planet; using Verse; namespace Multiplayer.Client.Experimental; -// Abstract class for ease of updating API - making sure that adding more methods or properties to the -// interface won't cause issues with other mods by implementing them here as virtual methods. -public abstract class Session : ISession +/// +/// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. +/// Sessions will be reset/reloaded during reloading - to prevent it, inherit from or . +/// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. +/// +public abstract class Session { // Use internal to prevent mods from easily modifying it? protected int sessionId; // Should it be virtual? + /// + /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. + /// Automatically applied by the session manager + /// If inheriting you don't have to worry about this property. + /// public int SessionId { get => sessionId; set => sessionId = value; } + /// + /// Used by the session manager while joining the game - if it returns it'll get removed. + /// public virtual bool IsSessionValid => true; - // For subclasses implementing IExplosableSession - public virtual void ExposeData() - { - Scribe_Values.Look(ref sessionId, "sessionId"); - } - + /// + /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. + /// + /// In case of , this will only be called if successfully added. public virtual void PostAddSession() { } + /// + /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. + /// public virtual void PostRemoveSession() { } @@ -47,7 +60,59 @@ protected static void SwitchToMapOrWorld(Map map) } } + /// + /// The map this session is used by or in case of global sessions. + /// public abstract Map Map { get; } + + /// + /// Called when checking ticking and if any session returns - it'll force pause the map/game. + /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. + /// + /// Current map (when checked from local session manager) or (when checked from local session manager). + /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . + /// if the session should pause the map/game, otherwise. public abstract bool IsCurrentlyPausing(Map map); + + /// + /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. + /// + /// Currently processed colonist bar entry. Will be called once per . + /// Menu option that will be displayed when the session is active. Can be . public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); } + +/// +/// Sessions inheriting from this class contain persistent data. +/// When inheriting from this class, remember to call base.ExposeData() to let it handle +/// Persistent data: +/// +/// Serialized into XML using RimWorld's Scribe system +/// Save-bound: survives a server restart +/// +/// +public abstract class ExposableSession : Session, IExposable +{ + // For subclasses implementing IExplosableSession + public virtual void ExposeData() + { + Scribe_Values.Look(ref sessionId, "sessionId"); + } +} + +/// +/// Sessions implementing this interface consist of semi-persistent data. +/// Semi-persistent data: +/// +/// Serialized into binary using the Sync system +/// Session-bound: survives a reload, lost when the server is closed +/// +/// +public abstract class SemiPersistentSession : Session +{ + /// + /// Writes/reads the data used by this session. + /// + /// Sync worker used for writing/reading the data. + public abstract void Sync(SyncWorker sync); +} diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index e52c6702..6eb466cb 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -6,8 +6,8 @@ using System.Reflection; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Factions; -using Multiplayer.Client.Persistent; using UnityEngine; using Verse; @@ -111,7 +111,7 @@ public void OnDestroy() ThingContext.Clear(); } - public IEnumerable GetSessions(Map map) + public IEnumerable GetSessions(Map map) { return worldComp.sessionManager.AllSessions.ConcatIfNotNull(map?.MpComp().sessionManager.AllSessions); } diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index e1d060dc..102fc812 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -9,7 +9,7 @@ namespace Multiplayer.Client { - public class CaravanFormingSession : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions + public class CaravanFormingSession : ExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions { public Map map; @@ -190,7 +190,7 @@ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry }); } - public bool CanExistWith(ISession other) => other is not CaravanFormingSession; + public bool CanExistWith(Session other) => other is not CaravanFormingSession; } } diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index 28359370..d7b399ef 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -11,7 +11,7 @@ namespace Multiplayer.Client.Persistent /// /// Represents an active Caravan Split session. This session will track all the pawns and items being split. /// - public class CaravanSplittingSession : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions + public class CaravanSplittingSession : ExposableSession, ISessionWithTransferables, IPausingWithDialog, ISessionWithCreationRestrictions { public override Map Map => null; @@ -173,6 +173,6 @@ public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry public override bool IsCurrentlyPausing(Map map) => true; - public bool CanExistWith(ISession other) => other is not CaravanSplittingSession; + public bool CanExistWith(Session other) => other is not CaravanSplittingSession; } } diff --git a/Source/Client/Persistent/ISession.cs b/Source/Client/Persistent/ISession.cs index 18c31ec6..c5390167 100644 --- a/Source/Client/Persistent/ISession.cs +++ b/Source/Client/Persistent/ISession.cs @@ -5,58 +5,6 @@ namespace Multiplayer.Client.Persistent { - /// - /// Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps. - /// Sessions will be reset/reloaded during reloading - to prevent it, implement or . - /// You should avoid implementing this interface directly, instead opting into inheriting for greater compatibility. - /// - public interface ISession - { - /// - /// The map this session is used by or in case of global sessions. - /// - Map Map { get; } - - /// - /// Used for syncing session across players by assigning them IDs, similarly to how every receives an ID. - /// Automatically applied by the session manager - /// If inheriting you don't have to worry about this property. - /// - int SessionId { get; set; } - - /// - /// Used by the session manager while joining the game - if it returns it'll get removed. - /// - bool IsSessionValid { get; } - - /// - /// Called when checking ticking and if any session returns - it'll force pause the map/game. - /// In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map. - /// - /// Current map (when checked from local session manager) or (when checked from local session manager). - /// If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned . - /// if the session should pause the map/game, otherwise. - bool IsCurrentlyPausing(Map map); - - /// - /// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options. - /// - /// Currently processed colonist bar entry. Will be called once per . - /// Menu option that will be displayed when the session is active. Can be . - FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry); - - /// - /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. - /// - /// In case of , this will only be called if successfully added. - void PostAddSession(); - - /// - /// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup. - /// - void PostRemoveSession(); - } - // todo unused for now public interface IPausingWithDialog { @@ -69,7 +17,7 @@ public interface IPausingWithDialog /// /// For safety, make sure to set [REF TO SETTER/METHOD] in and unset in . /// TODO: Replace [REF TO SETTER/METHOD] with actual ref in API - public interface ISessionWithTransferables : ISession + public interface ISessionWithTransferables { /// /// Used when syncing data across players, specifically to retrieve based on the it has. @@ -85,38 +33,6 @@ public interface ISessionWithTransferables : ISession void Notify_CountChanged(Transferable tr); } - /// - /// Sessions implementing this interface consist of persistent data. - /// When inheriting from , remember to call base.ExposeData() to let it handle - /// Persistent data: - /// - /// Serialized into XML using RimWorld's Scribe system - /// Save-bound: survives a server restart - /// - /// - /// A class should NOT implement both this and - it'll be treated as if only implementing this interface. - public interface IExposableSession : ISession, IExposable - { - } - - /// - /// Sessions implementing this interface consist of semi-persistent data. - /// Semi-persistent data: - /// - /// Serialized into binary using the Sync system - /// Session-bound: survives a reload, lost when the server is closed - /// - /// - /// A class should NOT implement both this and - it'll be treated as if only implementing . - public interface ISemiPersistentSession : ISession - { - /// - /// Writes/reads the data used by this session. - /// - /// Sync worker used for writing/reading the data. - void Sync(SyncWorker sync); - } - /// /// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type. /// @@ -129,7 +45,7 @@ public interface ISessionWithCreationRestrictions /// The other session the current one is checked against. Can be of different type. /// Currently only the current class checks against the existing ones - the existing classed don't check against this one. /// if the current session should be created, otherwise - bool CanExistWith(ISession other); + bool CanExistWith(Session other); } /// diff --git a/Source/Client/Persistent/PauseLockSession.cs b/Source/Client/Persistent/PauseLockSession.cs index 9a41158a..d26181a7 100644 --- a/Source/Client/Persistent/PauseLockSession.cs +++ b/Source/Client/Persistent/PauseLockSession.cs @@ -19,5 +19,5 @@ public class PauseLockSession : Session, ISessionWithCreationRestrictions // Should we add some message explaining pause locks/having a list of pausing ones? public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry) => null; - public bool CanExistWith(ISession other) => other is not PauseLockSession; + public bool CanExistWith(Session other) => other is not PauseLockSession; } diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index 02fdfa82..2c6463cd 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -12,7 +12,7 @@ namespace Multiplayer.Client.Persistent { - public class RitualSession : Session, IPausingWithDialog, ISemiPersistentSession + public class RitualSession : SemiPersistentSession, IPausingWithDialog { public Map map; public RitualData data; @@ -74,7 +74,7 @@ public void OpenWindow(bool sound = true) Find.WindowStack.Add(dialog); } - public void Sync(SyncWorker sync) + public override void Sync(SyncWorker sync) { if (sync.isWriting) { diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index f76ff7a9..769c56d0 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using HarmonyLib; using Multiplayer.Client.Experimental; using Multiplayer.Client.Saving; using Multiplayer.Common; @@ -12,20 +11,20 @@ namespace Multiplayer.Client.Persistent; public class SessionManager : IHasSemiPersistentData, ISessionManager { - public IReadOnlyList AllSessions => allSessions.AsReadOnly(); - public IReadOnlyList ExposableSessions => exposableSessions.AsReadOnly(); - public IReadOnlyList SemiPersistentSessions => semiPersistentSessions.AsReadOnly(); + public IReadOnlyList AllSessions => allSessions.AsReadOnly(); + public IReadOnlyList ExposableSessions => exposableSessions.AsReadOnly(); + public IReadOnlyList SemiPersistentSessions => semiPersistentSessions.AsReadOnly(); public IReadOnlyList TickingSessions => tickingSessions.AsReadOnly(); - private List allSessions = new(); - private List exposableSessions = new(); - private List semiPersistentSessions = new(); + private List allSessions = new(); + private List exposableSessions = new(); + private List semiPersistentSessions = new(); private List tickingSessions = new(); private static HashSet tempCleanupLoggingTypes = new(); public bool AnySessionActive => allSessions.Count > 0; - public bool AddSession(ISession session) + public bool AddSession(Session session) { if (GetFirstConflictingSession(session) != null) return false; @@ -34,7 +33,7 @@ public bool AddSession(ISession session) return true; } - public ISession GetOrAddSessionAnyConflict(ISession session) + public Session GetOrAddSessionAnyConflict(Session session) { if (GetFirstConflictingSession(session) is { } other) return other; @@ -43,7 +42,7 @@ public ISession GetOrAddSessionAnyConflict(ISession session) return session; } - public T GetOrAddSession(T session) where T : ISession + public T GetOrAddSession(T session) where T : Session { if (session is ISessionWithCreationRestrictions s) { @@ -69,11 +68,11 @@ public T GetOrAddSession(T session) where T : ISession return session; } - private void AddSessionNoCheck(ISession session) + private void AddSessionNoCheck(Session session) { - if (session is IExposableSession exposable) + if (session is ExposableSession exposable) exposableSessions.Add(exposable); - else if (session is ISemiPersistentSession semiPersistent) + else if (session is SemiPersistentSession semiPersistent) semiPersistentSessions.Add(semiPersistent); if (session is ITickingSession ticking) @@ -84,11 +83,11 @@ private void AddSessionNoCheck(ISession session) session.PostAddSession(); } - public bool RemoveSession(ISession session) + public bool RemoveSession(Session session) { - if (session is IExposableSession exposable) + if (session is ExposableSession exposable) exposableSessions.Remove(exposable); - else if (session is ISemiPersistentSession semiPersistent) + else if (session is SemiPersistentSession semiPersistent) semiPersistentSessions.Remove(semiPersistent); if (session is ITickingSession ticking) @@ -104,7 +103,7 @@ public bool RemoveSession(ISession session) return false; } - private ISession GetFirstConflictingSession(ISession session) + private Session GetFirstConflictingSession(Session session) { // Should the check be two-way? A property for optional two-way check? if (session is ISessionWithCreationRestrictions restrictions) @@ -122,11 +121,11 @@ public void TickSessions() tickingSessions[i].Tick(); } - public T GetFirstOfType() where T : ISession => allSessions.OfType().FirstOrDefault(); + public T GetFirstOfType() where T : Session => allSessions.OfType().FirstOrDefault(); - public T GetFirstWithId(int id) where T : ISession => allSessions.OfType().FirstOrDefault(s => s.SessionId == id); + public T GetFirstWithId(int id) where T : Session => allSessions.OfType().FirstOrDefault(s => s.SessionId == id); - public ISession GetFirstWithId(int id) => allSessions.FirstOrDefault(s => s.SessionId == id); + public Session GetFirstWithId(int id) => allSessions.FirstOrDefault(s => s.SessionId == id); public void WriteSemiPersistent(ByteWriter data) { @@ -170,7 +169,7 @@ public void ReadSemiPersistent(ByteReader data) { var sessionsCount = data.ReadInt32(); semiPersistentSessions.Clear(); - allSessions.RemoveAll(s => s is ISemiPersistentSession); + allSessions.RemoveAll(s => s is SemiPersistentSession); for (int i = 0; i < sessionsCount; i++) { @@ -198,7 +197,7 @@ public void ReadSemiPersistent(ByteReader data) try { - if (Activator.CreateInstance(objType, map) is ISemiPersistentSession session) + if (Activator.CreateInstance(objType, map) is SemiPersistentSession session) { session.SessionId = sessionId; session.Sync(new ReadingSyncWorker(data)); @@ -242,7 +241,7 @@ public void ExposeSessions() tempCleanupLoggingTypes.Clear(); // Just in case something went wrong when exposing data, clear the all session from exposable ones and fill them again - allSessions.RemoveAll(s => s is IExposableSession); + allSessions.RemoveAll(s => s is ExposableSession); allSessions.AddRange(exposableSessions); } } @@ -257,21 +256,4 @@ public bool IsAnySessionCurrentlyPausing(Map map) return false; } - - public static void ValidateSessionClasses() - { - foreach (var subclass in typeof(ISession).AllSubclasses()) - { - var interfaces = subclass.GetInterfaces(); - - if (interfaces.Contains(typeof(ISemiPersistentSession))) - { - if (interfaces.Contains(typeof(IExposableSession))) - Log.Error($"Type {subclass} implements both {nameof(IExposableSession)} and {nameof(ISemiPersistentSession)}, it should implement only one of them at most."); - - if (AccessTools.GetDeclaredConstructors(subclass).All(c => c.GetParameters().Length != 1 || c.GetParameters()[0].ParameterType != typeof(Map))) - Log.Error($"Type {subclass} implements {nameof(ISemiPersistentSession)}, but does not have a single parameter constructor with {nameof(Map)} as the parameter."); - } - } - } } diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index cbcc77da..0dcca709 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -14,7 +14,7 @@ namespace Multiplayer.Client { - public class MpTradeSession : Session, IExposableSession, ISessionWithTransferables, ISessionWithCreationRestrictions, ITickingSession + public class MpTradeSession : ExposableSession, ISessionWithTransferables, ISessionWithCreationRestrictions, ITickingSession { public static MpTradeSession current; @@ -48,7 +48,7 @@ private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) giftsOnly = giftMode; } - public bool CanExistWith(ISession other) + public bool CanExistWith(Session other) { if (other is not MpTradeSession otherTrade) return true; diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index 329ad2e1..db3a89fa 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Client { - public class TransporterLoading : Session, IExposableSession, ISessionWithTransferables, IPausingWithDialog + public class TransporterLoading : ExposableSession, ISessionWithTransferables, IPausingWithDialog { public override Map Map => map; diff --git a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs index 3be1743f..b3fb7e21 100644 --- a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs +++ b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Multiplayer.Client.Comp; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Persistent; using Multiplayer.Common; using RimWorld; @@ -27,7 +28,7 @@ public static class SyncDictMultiplayer } }, { - (ByteWriter data, ISession session) => + (ByteWriter data, Session session) => { data.MpContext().map ??= session.Map; data.WriteInt32(session.SessionId); diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index b44bda0a..a3f45518 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -13,6 +13,6 @@ public static class ImplSerialization public static void Init() { syncSimples = TypeUtil.AllImplementationsOrdered(typeof(ISyncSimple)); - sessions = TypeUtil.AllImplementationsOrdered(typeof(ISession)); + sessions = TypeUtil.AllImplementationsOrdered(typeof(Session)); } } From f85c6b4c8549eefce62c3acdf76d0e9f3bfb9455 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Sat, 11 Nov 2023 19:59:17 +0100 Subject: [PATCH 27/29] Added methods to `MultiplayerAPIBridge` that will (likely) end up in MP API This is done in preparation of an update to MP API --- Source/Client/MultiplayerAPIBridge.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Source/Client/MultiplayerAPIBridge.cs b/Source/Client/MultiplayerAPIBridge.cs index af8c7664..76c229c2 100644 --- a/Source/Client/MultiplayerAPIBridge.cs +++ b/Source/Client/MultiplayerAPIBridge.cs @@ -2,6 +2,9 @@ using System.Reflection; using Multiplayer.API; using Multiplayer.Client; +using Multiplayer.Client.Experimental; +using Multiplayer.Client.Persistent; +using Verse; // ReSharper disable once CheckNamespace namespace Multiplayer.Common @@ -119,5 +122,22 @@ public void RegisterPauseLock(PauseLockDelegate pauseLock) { Sync.RegisterPauseLock(pauseLock); } + + public ISessionManager GetGlobalSessionManager() + { + return Client.Multiplayer.WorldComp.sessionManager; + } + + public ISessionManager GetLocalSessionManager(Map map) + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + return map.MpComp().sessionManager; + } + + public void SetCurrentSessionWithTransferables(ISessionWithTransferables session) + { + SyncSessionWithTransferablesMarker.DrawnSessionWithTransferables = session; + } } } From ccf0b22f48bedb4f57afb85b8f746e815d7a22f1 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Tue, 14 Nov 2023 21:57:31 +0100 Subject: [PATCH 28/29] Fix session map syncing - Added a single parameter (Map) constructor to all `Session` subclasses with docs to inform devs using the API of a mandatory constructor - `ExposableSession` will now be provided the `Map` parameter by the `SessionManager` - `SemiPersistentSessions` won't sync a map on a per-session basis, instead being provided the `SessionManager` map --- Source/Client/Comp/Map/MultiplayerMapComp.cs | 3 ++- .../Client/Comp/World/MultiplayerWorldComp.cs | 4 ++-- Source/Client/Experimental/Session.cs | 12 ++++++++++ .../Persistent/CaravanFormingSession.cs | 2 +- .../Persistent/CaravanSplittingSession.cs | 8 +++++-- Source/Client/Persistent/PauseLockSession.cs | 2 ++ Source/Client/Persistent/Rituals.cs | 5 ++--- Source/Client/Persistent/SessionManager.cs | 22 +++++++------------ Source/Client/Persistent/Trading.cs | 4 ++-- .../Persistent/TransporterLoadingSession.cs | 2 +- 10 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index f9b4a26b..641e4c7d 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -23,7 +23,7 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Dictionary factionData = new(); public Dictionary customFactionData = new(); - public SessionManager sessionManager = new(); + public SessionManager sessionManager; public List mapDialogs = new(); public int autosaveCounter; @@ -33,6 +33,7 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public MultiplayerMapComp(Map map) { this.map = map; + sessionManager = new(map); } public CaravanFormingSession CreateCaravanFormingSession(bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? meetingSpot = null) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index 2e32d56c..86f1374d 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -17,7 +17,7 @@ public class MultiplayerWorldComp : IHasSemiPersistentData public TileTemperaturesComp uiTemperatures; public List trading = new(); // Should only be modified from MpTradeSession in PostAdd/Remove and ExposeData - public SessionManager sessionManager = new(); + public SessionManager sessionManager = new(null); public Faction spectatorFaction; @@ -39,7 +39,7 @@ public void ExposeData() sessionManager.ExposeSessions(); // Ensure a pause lock session exists if there's any pause locks registered if (!PauseLockSession.pauseLocks.NullOrEmpty()) - sessionManager.AddSession(new PauseLockSession()); + sessionManager.AddSession(new PauseLockSession(null)); DoBackCompat(); } diff --git a/Source/Client/Experimental/Session.cs b/Source/Client/Experimental/Session.cs index a44ce9e3..a779dacd 100644 --- a/Source/Client/Experimental/Session.cs +++ b/Source/Client/Experimental/Session.cs @@ -32,6 +32,12 @@ public int SessionId /// public virtual bool IsSessionValid => true; + /// + /// Mandatory constructor for any subclass of . + /// + /// The map this session belongs to. It will be provided by session manager when syncing. + protected Session(Map map) { } + /// /// Called once the sessions has been added to the list of active sessions. Can be used for initialization. /// @@ -93,6 +99,9 @@ protected static void SwitchToMapOrWorld(Map map) /// public abstract class ExposableSession : Session, IExposable { + /// + protected ExposableSession(Map map) : base(map) { } + // For subclasses implementing IExplosableSession public virtual void ExposeData() { @@ -110,6 +119,9 @@ public virtual void ExposeData() /// public abstract class SemiPersistentSession : Session { + /// + protected SemiPersistentSession(Map map) : base(map) { } + /// /// Writes/reads the data used by this session. /// diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 102fc812..ccb97c73 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -26,7 +26,7 @@ public class CaravanFormingSession : ExposableSession, ISessionWithTransferables public override Map Map => map; - public CaravanFormingSession(Map map) + public CaravanFormingSession(Map map) : base(map) { this.map = map; } diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index d7b399ef..cf80026c 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using Verse; using RimWorld; using RimWorld.Planet; @@ -35,12 +36,15 @@ public class CaravanSplittingSession : ExposableSession, ISessionWithTransferabl /// public CaravanSplittingProxy dialog; + public CaravanSplittingSession(Map map) : base(null) + { + } + /// /// Handles creation of new CaravanSplittingSession. - /// Ensures a unique Id is given to this session and creates the dialog. /// /// - public CaravanSplittingSession(Caravan caravan) + public CaravanSplittingSession(Caravan caravan) : base(null) { Caravan = caravan; diff --git a/Source/Client/Persistent/PauseLockSession.cs b/Source/Client/Persistent/PauseLockSession.cs index d26181a7..be5730e5 100644 --- a/Source/Client/Persistent/PauseLockSession.cs +++ b/Source/Client/Persistent/PauseLockSession.cs @@ -14,6 +14,8 @@ public class PauseLockSession : Session, ISessionWithCreationRestrictions public override Map Map => null; + public PauseLockSession(Map _) : base(null) { } + public override bool IsCurrentlyPausing(Map map) => pauseLocks.Any(x => x(map)); // Should we add some message explaining pause locks/having a list of pausing ones? diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index 2c6463cd..65dfa42c 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -19,14 +19,13 @@ public class RitualSession : SemiPersistentSession, IPausingWithDialog public override Map Map => map; - public RitualSession(Map map) + public RitualSession(Map map) : base(map) { this.map = map; } - public RitualSession(Map map, RitualData data) + public RitualSession(Map map, RitualData data) : this(map) { - this.map = map; this.data = data; this.data.assignments.session = this; } diff --git a/Source/Client/Persistent/SessionManager.cs b/Source/Client/Persistent/SessionManager.cs index 769c56d0..31ac2df6 100644 --- a/Source/Client/Persistent/SessionManager.cs +++ b/Source/Client/Persistent/SessionManager.cs @@ -22,8 +22,14 @@ public class SessionManager : IHasSemiPersistentData, ISessionManager private List tickingSessions = new(); private static HashSet tempCleanupLoggingTypes = new(); + public Map Map { get; } public bool AnySessionActive => allSessions.Count > 0; + public SessionManager(Map map) + { + Map = map; + } + public bool AddSession(Session session) { if (GetFirstConflictingSession(session) != null) @@ -151,7 +157,6 @@ public void WriteSemiPersistent(ByteWriter data) foreach (var session in semiPersistentSessions) { data.WriteUShort((ushort)ImplSerialization.sessions.FindIndex(session.GetType())); - data.WriteInt32(session.Map?.uniqueID ?? -1); data.WriteInt32(session.SessionId); try @@ -174,7 +179,6 @@ public void ReadSemiPersistent(ByteReader data) for (int i = 0; i < sessionsCount; i++) { ushort typeIndex = data.ReadUShort(); - int mapId = data.ReadInt32(); int sessionId = data.ReadInt32(); if (typeIndex >= ImplSerialization.sessions.Length) @@ -184,20 +188,10 @@ public void ReadSemiPersistent(ByteReader data) } var objType = ImplSerialization.sessions[typeIndex]; - Map map = null; - if (mapId != -1) - { - map = Find.Maps.FirstOrDefault(m => m.uniqueID == mapId); - if (map == null) - { - Log.Error($"Trying to read semi persistent session of type {objType} received a null map while expecting a map with ID {mapId}"); - // Continue? Let it run? - } - } try { - if (Activator.CreateInstance(objType, map) is SemiPersistentSession session) + if (Activator.CreateInstance(objType, Map) is SemiPersistentSession session) { session.SessionId = sessionId; session.Sync(new ReadingSyncWorker(data)); @@ -214,7 +208,7 @@ public void ReadSemiPersistent(ByteReader data) public void ExposeSessions() { - Scribe_Collections.Look(ref exposableSessions, "sessions", LookMode.Deep); + Scribe_Collections.Look(ref exposableSessions, "sessions", LookMode.Deep, Map); if (Scribe.mode == LoadSaveMode.PostLoadInit) { diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 0dcca709..2a768e9e 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -38,9 +38,9 @@ public string Label public override bool IsSessionValid => trader != null && playerNegotiator != null; - public MpTradeSession() { } + public MpTradeSession(Map _) : base(null) { } - private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) + private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) : base(null) { this.trader = trader; this.playerNegotiator = playerNegotiator; diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index db3a89fa..1da106d0 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -20,7 +20,7 @@ public class TransporterLoading : ExposableSession, ISessionWithTransferables, I public bool uiDirty; - public TransporterLoading(Map map) + public TransporterLoading(Map map) : base(map) { this.map = map; } From ba19d254134e67f044f7fd72d7a06d05b97cd153 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon Date: Wed, 15 Nov 2023 23:57:53 +0100 Subject: [PATCH 29/29] Added a sync dict entry for `ISessionWithTransferables` It first checks if it's a subtype of `Session`, and will show an error if it isn't --- .../Client/Syncing/Dict/SyncDictMultiplayer.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs index b3fb7e21..1bff4879 100644 --- a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs +++ b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs @@ -38,6 +38,22 @@ public static class SyncDictMultiplayer return Multiplayer.game.GetSessions(data.MpContext().map).FirstOrDefault(s => s.SessionId == id); }, true }, + { + (ByteWriter data, ISessionWithTransferables session) => + { + if (session is Session s) + { + WriteSync(data, s); + return; + } + + WriteSync(data, null); + if (session != null) + Log.ErrorOnce($"Trying to sync {nameof(ISessionWithTransferables)} that is not a subtype of {nameof(Session)}", session.GetHashCode()); + }, + (ByteReader data) => ReadSync(data) as ISessionWithTransferables, + true + }, #endregion #region Multiplayer Transferables