From f162035ae0a5ffec72389ff980732d0c62acd09a Mon Sep 17 00:00:00 2001 From: Zetrith Date: Tue, 3 Oct 2023 00:12:27 +0200 Subject: [PATCH 1/5] Update 0.9, multifaction - Protocol 34 - Multifaction: new hosting option, faction creation screen, set faction context in more places, refactor code related to factions - Remove id blocks - Track getting unique ids in desync traces - Log stack traces in Sync command write log - Use my UI layout helper (Layouter.cs) from Prepatcher - Only set faction context when necessary (should slightly improve performance) - Move Native and DeferredStackTracingImpl to Common - Reorganize project dependencies (they previously doubled as artifact output control) - Move conversion to singleplayer into its own file - Split player cursor and location ping code - Remove HotSwappableAttribute - Fix applying the mod list with "Fix and restart" - Fix issues with desync tracing and latest Harmony - Fix desyncs related to autosaving and battle log - Fix exception when not including all arguments in SyncMethod exposeParameters - Fix clicking on files in save list sometimes not working --- .gitignore | 4 +- Source/Client/AsyncTime/AsyncTimeComp.cs | 57 +- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 57 +- Source/Client/AsyncTime/StorytellerPatches.cs | 3 +- .../Client/Comp/Game/MultiplayerGameComp.cs | 19 +- Source/Client/Comp/Map/ExposeActor.cs | 2 +- Source/Client/Comp/Map/FactionMapData.cs | 5 +- Source/Client/Comp/Map/MultiplayerMapComp.cs | 26 +- .../Client/Comp/World/MultiplayerWorldComp.cs | 13 +- Source/Client/ConstantTicker.cs | 9 +- Source/Client/Debug/DebugActions.cs | 24 - Source/Client/Debug/DebugPatches.cs | 4 +- Source/Client/Debug/DebugTools.cs | 8 +- Source/Client/Desyncs/DeferredStackTracing.cs | 246 +-------- Source/Client/Desyncs/StackTraceLogItem.cs | 16 +- Source/Client/Desyncs/SyncCoordinator.cs | 3 +- Source/Client/EarlyInit.cs | 20 +- .../Client/Factions/AutoRoofFactionPatch.cs | 41 ++ Source/Client/Factions/FactionContext.cs | 4 +- Source/Client/Factions/FactionCreator.cs | 168 ++++++ Source/Client/Factions/FactionExtensions.cs | 43 ++ Source/Client/Factions/FactionRepeater.cs | 3 +- Source/Client/Factions/FactionSidebar.cs | 174 +++++++ .../{Patches => Factions}/Forbiddables.cs | 0 Source/Client/Factions/Multifaction.cs | 81 --- Source/Client/Factions/MultifactionPatches.cs | 322 ++++++++++++ Source/Client/Factions/SidebarPatch.cs | 33 ++ Source/Client/Multiplayer.cs | 13 +- Source/Client/Multiplayer.csproj | 28 +- Source/Client/MultiplayerGame.cs | 17 +- Source/Client/MultiplayerSession.cs | 17 +- Source/Client/MultiplayerStatic.cs | 31 +- Source/Client/Networking/HostUtil.cs | 135 ++--- Source/Client/Networking/JoinData.cs | 10 +- .../Networking/State/ClientJoiningState.cs | 2 - .../Networking/State/ClientLoadingState.cs | 1 + .../Networking/State/ClientPlayingState.cs | 3 +- Source/Client/OnMainThread.cs | 2 +- Source/Client/Patches/Determinism.cs | 24 +- Source/Client/Patches/EarlyPatchAttribute.cs | 2 +- Source/Client/Patches/Letters.cs | 2 +- Source/Client/Patches/MapSetup.cs | 75 +++ Source/Client/Patches/Optimizations.cs | 5 +- Source/Client/Patches/Patches.cs | 142 +---- Source/Client/Patches/Seeds.cs | 16 +- Source/Client/Patches/StylingStation.cs | 2 +- Source/Client/Patches/ThingMethodPatches.cs | 37 +- Source/Client/Patches/TickPatch.cs | 13 +- Source/Client/Patches/TileTemperatures.cs | 1 - Source/Client/Patches/UniqueIds.cs | 115 ++-- Source/Client/Patches/VanillaTweaks.cs | 3 +- Source/Client/Patches/WorldPawns.cs | 1 - .../Persistent/CaravanFormingSession.cs | 7 +- .../Persistent/CaravanSplittingPatches.cs | 2 +- .../Persistent/CaravanSplittingProxy.cs | 2 +- .../Persistent/CaravanSplittingSession.cs | 5 +- Source/Client/Persistent/PersistentDialogs.cs | 35 +- Source/Client/Persistent/RitualData.cs | 109 +--- Source/Client/Persistent/Rituals.cs | 2 +- Source/Client/Persistent/Trading.cs | 10 +- .../Persistent/TransporterLoadingSession.cs | 2 +- Source/Client/Saving/ConvertToSp.cs | 47 ++ Source/Client/Saving/CrossRefs.cs | 4 +- Source/Client/Saving/Loader.cs | 13 +- Source/Client/Saving/Replay.cs | 12 +- Source/Client/Saving/ReplayConnection.cs | 20 +- Source/Client/Saving/SavingPatches.cs | 36 +- Source/Client/Saving/Scribe_Custom.cs | 26 - Source/Client/Settings/MpSettings.cs | 2 +- Source/Client/Syncing/DefSerialization.cs | 2 +- .../Client/Syncing/DelegateSerialization.cs | 158 ++++++ .../Client/Syncing/ExposableSerialization.cs | 2 +- Source/Client/Syncing/Game/SyncDelegates.cs | 11 +- Source/Client/Syncing/Handler/SyncDelegate.cs | 4 +- Source/Client/Syncing/Handler/SyncField.cs | 1 + Source/Client/Syncing/Logger/LogNode.cs | 2 +- Source/Client/Syncing/Sync.cs | 16 +- Source/Client/UI/AlertPing.cs | 8 +- Source/Client/UI/CursorAndPing.cs | 258 --------- Source/Client/UI/CursorPatches.cs | 134 +++++ Source/Client/UI/DrawPingMap.cs | 2 +- Source/Client/UI/DrawPingPlanet.cs | 2 +- Source/Client/UI/IngameDebug.cs | 24 +- Source/Client/UI/IngameUI.cs | 9 +- Source/Client/UI/Layouter.cs | 493 ++++++++++++++++++ Source/Client/UI/LocationPings.cs | 85 +++ Source/Client/UI/MainMenuAnimation.cs | 5 +- Source/Client/UI/MainMenuPatches.cs | 49 +- Source/Client/UI/PingInfo.cs | 93 ++++ Source/Client/UI/PlayerCursors.cs | 180 +++---- Source/Client/UI/ReplayTimeline.cs | 28 +- Source/Client/Util/Extensions.cs | 40 -- Source/Client/Util/MethodOf.cs | 70 +++ Source/Client/Util/MpUI.cs | 1 - Source/Client/Util/MpUtil.cs | 3 +- Source/Client/Windows/DesyncedWindow.cs | 3 - Source/Client/Windows/HostWindow.cs | 33 +- Source/Client/Windows/JoinDataWindow.cs | 13 +- Source/Client/Windows/PacketLogWindow.cs | 18 +- Source/Client/Windows/SaveFileReader.cs | 2 + Source/Client/Windows/SaveGameWindow.cs | 8 +- Source/Client/Windows/ServerBrowser.cs | 20 +- Source/Common/ChatCommands.cs | 6 +- Source/Common/CommandHandler.cs | 1 + Source/Common/CommandType.cs | 19 + Source/Common/Commands.cs | 24 - Source/Common/Common.csproj | 2 +- Source/Common/DeferredStackTracingImpl.cs | 262 ++++++++++ Source/Common/IdBlock.cs | 46 -- Source/Common/LiteNetManager.cs | 3 +- Source/Common/MultiplayerServer.cs | 12 +- Source/{Client => Common}/Native.cs | 56 +- Source/Common/Networking/ConnectionBase.cs | 10 +- Source/Common/Networking/Packets.cs | 1 - .../Networking/State/ServerJoiningState.cs | 2 +- .../Networking/State/ServerLoadingState.cs | 2 +- .../Networking/State/ServerPlayingState.cs | 36 +- Source/Common/PlayerManager.cs | 2 +- Source/Common/ReplayInfo.cs | 13 +- Source/Common/ServerSettings.cs | 4 + Source/Common/Util/CompilerTypes.cs | 15 + Source/Common/Util/DynDelegate.cs | 2 +- Source/Common/Util/Endpoints.cs | 5 +- Source/Common/Util/Extensions.cs | 4 +- Source/Common/Util/HotSwappableAttribute.cs | 8 - Source/Common/Util/ZipExtensions.cs | 1 - Source/Common/Version.cs | 4 +- Source/Common/WorldData.cs | 3 +- Source/Multiplayer.sln | 6 + Source/MultiplayerLoader/MultiplayerLoader.cs | 5 +- .../MultiplayerLoader.csproj | 6 +- Source/Server/Server.cs | 3 +- Source/Tests/Tests.csproj | 1 + Source/TestsOnMono/Program.cs | 25 + Source/TestsOnMono/TestsOnMono.csproj | 15 + 135 files changed, 3043 insertions(+), 1754 deletions(-) create mode 100644 Source/Client/Factions/AutoRoofFactionPatch.cs create mode 100644 Source/Client/Factions/FactionCreator.cs create mode 100644 Source/Client/Factions/FactionExtensions.cs create mode 100644 Source/Client/Factions/FactionSidebar.cs rename Source/Client/{Patches => Factions}/Forbiddables.cs (100%) delete mode 100644 Source/Client/Factions/Multifaction.cs create mode 100644 Source/Client/Factions/MultifactionPatches.cs create mode 100644 Source/Client/Factions/SidebarPatch.cs create mode 100644 Source/Client/Patches/MapSetup.cs create mode 100644 Source/Client/Saving/ConvertToSp.cs create mode 100644 Source/Client/Syncing/DelegateSerialization.cs delete mode 100644 Source/Client/UI/CursorAndPing.cs create mode 100644 Source/Client/UI/CursorPatches.cs create mode 100644 Source/Client/UI/Layouter.cs create mode 100644 Source/Client/UI/LocationPings.cs create mode 100644 Source/Client/UI/PingInfo.cs create mode 100644 Source/Client/Util/MethodOf.cs create mode 100644 Source/Common/CommandType.cs delete mode 100644 Source/Common/Commands.cs create mode 100644 Source/Common/DeferredStackTracingImpl.cs delete mode 100644 Source/Common/IdBlock.cs rename Source/{Client => Common}/Native.cs (83%) delete mode 100644 Source/Common/Util/HotSwappableAttribute.cs create mode 100644 Source/TestsOnMono/Program.cs create mode 100644 Source/TestsOnMono/TestsOnMono.csproj diff --git a/.gitignore b/.gitignore index 97724ca5..f93c7ab3 100644 --- a/.gitignore +++ b/.gitignore @@ -270,4 +270,6 @@ __pycache__/ /Multiplayer Multiplayer*.zip -!Source/Client/Debug/ \ No newline at end of file +!Source/Client/Debug/ + +/Source/mpdb \ No newline at end of file diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index a135ae04..a60c5ab1 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -7,6 +7,7 @@ using Multiplayer.API; using Verse; using Multiplayer.Client.Comp; +using Multiplayer.Client.Factions; using Multiplayer.Client.Patches; using Multiplayer.Client.Saving; using Multiplayer.Client.Util; @@ -67,7 +68,7 @@ public void SetDesiredTimeSpeed(TimeSpeed speed) public float TimeToTickThrough { get; set; } - public Queue Cmds { get => cmds; } + public Queue Cmds => cmds; public int TickableId => map.uniqueID; @@ -148,10 +149,7 @@ public void TickMapTrading() if (session.playerNegotiator.Map != map) continue; if (session.ShouldCancel()) - { Multiplayer.WorldComp.RemoveTradeSession(session); - continue; - } } } @@ -183,9 +181,6 @@ public void PreContext() Current.Game.storyteller = storyteller; Current.Game.storyWatcher = storyWatcher; - //UniqueIdsPatch.CurrentBlock = map.MpComp().mapIdBlock; - UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; - Rand.PushState(); Rand.StateCompressed = randState; @@ -195,8 +190,6 @@ public void PreContext() public void PostContext() { - UniqueIdsPatch.CurrentBlock = null; - Current.Game.storyteller = prevStoryteller; Current.Game.storyWatcher = prevStoryWatcher; @@ -276,11 +269,6 @@ public void ExecuteCmd(ScheduledCommand cmd) MpDebugTools.HandleCmd(data); } - if (cmdType == CommandType.CreateMapFactionData) - { - HandleMapFactionData(cmd, data); - } - if (cmdType == CommandType.MapTimeSpeed && Multiplayer.GameComp.asyncTime) { TimeSpeed speed = (TimeSpeed)data.ReadByte(); @@ -289,19 +277,9 @@ public void ExecuteCmd(ScheduledCommand cmd) MpLog.Debug("Set map time speed " + speed); } - if (cmdType == CommandType.MapIdBlock) - { - IdBlock block = IdBlock.Deserialize(data); - - if (map != null) - { - //map.MpComp().mapIdBlock = block; - } - } - if (cmdType == CommandType.Designator) { - HandleDesignator(cmd, data); + HandleDesignator(data); } UpdateManagers(); @@ -357,28 +335,11 @@ private static void TrySetCurrentMap(Map map) } } - private void HandleMapFactionData(ScheduledCommand cmd, ByteReader data) - { - int factionId = data.ReadInt32(); - - Faction faction = Find.FactionManager.GetById(factionId); - MultiplayerMapComp comp = map.MpComp(); - - if (!comp.factionData.ContainsKey(factionId)) - { - BeforeMapGeneration.InitNewMapFactionData(map, faction); - MpLog.Log($"New map faction data for {faction.GetUniqueLoadID()}"); - } - } - - private void HandleDesignator(ScheduledCommand command, ByteReader data) + private void HandleDesignator(ByteReader data) { - var mode = SyncSerialization.ReadSync(data); - var designator = SyncSerialization.ReadSync(data); - Container? prevArea = null; - bool SetState(Designator designator, ByteReader data) + bool SetState(Designator designator) { if (designator is Designator_AreaAllowed) { @@ -415,9 +376,12 @@ void RestoreState() DesignatorInstall_SetThingToInstall.thingToInstall = null; } + var mode = SyncSerialization.ReadSync(data); + var designator = SyncSerialization.ReadSync(data); + try { - if (!SetState(designator, data)) return; + if (!SetState(designator)) return; if (mode == DesignatorMode.SingleCell) { @@ -454,9 +418,8 @@ private void CacheNothingHappening() nothingHappeningCached = true; var list = map.mapPawns.SpawnedPawnsInFaction(Faction.OfPlayer); - for (int j = 0; j < list.Count; j++) + foreach (var pawn in list) { - Pawn pawn = list[j]; if (pawn.HostFaction == null && pawn.RaceProps.Humanlike && pawn.Awake()) nothingHappeningCached = false; } diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index c9a6a421..d3f32336 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -4,7 +4,7 @@ using HarmonyLib; using Multiplayer.Client.Comp; using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Patches; +using Multiplayer.Client.Factions; using Multiplayer.Client.Saving; using Multiplayer.Client.Util; using Multiplayer.Common; @@ -45,10 +45,12 @@ public float TickRateMultiplier(TimeSpeed speed) }; } - // Run at the speed of the fastest map - public TimeSpeed DesiredTimeSpeed => Find.Maps.Select(m => m.AsyncTime()) + // Run at the speed of the fastest map or at chosen speed if there are no maps + public TimeSpeed DesiredTimeSpeed => !Find.Maps.Any() ? + timeSpeedInt : + Find.Maps.Select(m => m.AsyncTime()) .Where(a => a.ActualRateMultiplier(a.DesiredTimeSpeed) != 0f) - .Max(a => a?.DesiredTimeSpeed) ?? timeSpeedInt; + .Max(a => a?.DesiredTimeSpeed) ?? TimeSpeed.Paused; public void SetDesiredTimeSpeed(TimeSpeed speed) { @@ -137,16 +139,18 @@ public void Tick() public void PreContext() { Find.TickManager.CurTimeSpeed = DesiredTimeSpeed; - UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; Rand.PushState(); Rand.StateCompressed = randState; + + FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction); } public void PostContext() { + FactionExtensions.PopFaction(); + randState = Rand.StateCompressed; Rand.PopState(); - UniqueIdsPatch.CurrentBlock = null; } public void ExecuteCmd(ScheduledCommand cmd) @@ -159,7 +163,7 @@ public void ExecuteCmd(ScheduledCommand cmd) TickPatch.currentExecutingCmdIssuedBySelf = cmd.issuedBySelf && !TickPatch.Simulating; PreContext(); - Extensions.PushFaction(null, cmd.GetFaction()); + FactionExtensions.PushFaction(null, cmd.GetFaction()); bool prevDevMode = Prefs.data.devMode; var prevGodMode = DebugSettings.godMode; @@ -195,14 +199,9 @@ public void ExecuteCmd(ScheduledCommand cmd) SetTimeEverywhere(TimeSpeed.Paused); } - if (cmdType == CommandType.SetupFaction) - { - HandleSetupFaction(cmd, data); - } - if (cmdType == CommandType.CreateJoinPoint) { - LongEventHandler.QueueLongEvent(CreateJoinPoint, "MpCreatingJoinPoint", false, null); + LongEventHandler.QueueLongEvent(CreateJoinPointAndSendIfHost, "MpCreatingJoinPoint", false, null); } if (cmdType == CommandType.InitPlayerData) @@ -224,7 +223,7 @@ public void ExecuteCmd(ScheduledCommand cmd) MpLog.Debug($"rand calls {DeferredStackTracing.randCalls - randCalls1}"); MpLog.Debug("rand state " + Rand.StateCompressed); - Extensions.PopFaction(); + FactionExtensions.PopFaction(); PostContext(); TickPatch.currentExecutingCmdIssuedBySelf = false; executingCmdWorld = false; @@ -236,7 +235,7 @@ public void ExecuteCmd(ScheduledCommand cmd) } } - private static void CreateJoinPoint() + private static void CreateJoinPointAndSendIfHost() { Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); @@ -288,34 +287,6 @@ private void HandleTimeVote(ScheduledCommand cmd, ByteReader data) tickable.SetDesiredTimeSpeed(Multiplayer.GameComp.GetLowestTimeVote(tickableId)); } - private void HandleSetupFaction(ScheduledCommand command, ByteReader data) - { - int factionId = data.ReadInt32(); - Faction faction = Find.FactionManager.GetById(factionId); - - if (faction == null) - { - faction = new Faction - { - loadID = factionId, - def = FactionDefOf.PlayerColony, - Name = "Multiplayer faction", - }; - - Find.FactionManager.Add(faction); - - foreach (Faction current in Find.FactionManager.AllFactionsListForReading) - { - if (current == faction) continue; - current.TryMakeInitialRelationsWith(faction); - } - - Multiplayer.WorldComp.factionData[factionId] = FactionWorldData.New(factionId); - - MpLog.Log($"New faction {faction.GetUniqueLoadID()}"); - } - } - public void FinalizeInit() { Multiplayer.game.SetThingMakerSeed((int)(randState >> 32)); diff --git a/Source/Client/AsyncTime/StorytellerPatches.cs b/Source/Client/AsyncTime/StorytellerPatches.cs index ce445145..f7940fc5 100644 --- a/Source/Client/AsyncTime/StorytellerPatches.cs +++ b/Source/Client/AsyncTime/StorytellerPatches.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using HarmonyLib; diff --git a/Source/Client/Comp/Game/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs index 60abf1e5..b3ab0122 100644 --- a/Source/Client/Comp/Game/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -11,38 +11,31 @@ namespace Multiplayer.Client.Comp public class MultiplayerGameComp : IExposable, IHasSemiPersistentData { public bool asyncTime; + public bool multifaction; public bool debugMode; public bool logDesyncTraces; public PauseOnLetter pauseOnLetter; public TimeControl timeControl; public Dictionary playerData = new(); // player id to player data - public IdBlock globalIdBlock = new(int.MaxValue / 2, 1_000_000_000); + public string idBlockBase64; public bool IsLowestWins => timeControl == TimeControl.LowestWins; public PlayerData LocalPlayerDataOrNull => playerData.GetValueOrDefault(Multiplayer.session.playerId); - public MultiplayerGameComp(Game game) - { - } - public void ExposeData() { Scribe_Values.Look(ref asyncTime, "asyncTime", true, true); + Scribe_Values.Look(ref multifaction, "multifaction", false, true); Scribe_Values.Look(ref debugMode, "debugMode"); Scribe_Values.Look(ref logDesyncTraces, "logDesyncTraces"); Scribe_Values.Look(ref pauseOnLetter, "pauseOnLetter"); Scribe_Values.Look(ref timeControl, "timeControl"); - Scribe_Custom.LookIdBlock(ref globalIdBlock, "globalIdBlock"); - - if (globalIdBlock == null) - { - // todo globalIdBlock was previously in WorldComp, this is a quick hack to make old saves compatible - Log.Warning("Global id block was null, fixing..."); - globalIdBlock = new IdBlock(int.MaxValue / 2, 1_000_000_000); - } + // Store for back-compat conversion in GameExposeComponentsPatch + if (Scribe.mode == LoadSaveMode.LoadingVars) + Scribe_Values.Look(ref idBlockBase64, "globalIdBlock"); } public void WriteSemiPersistent(ByteWriter writer) diff --git a/Source/Client/Comp/Map/ExposeActor.cs b/Source/Client/Comp/Map/ExposeActor.cs index db59388a..248aa201 100644 --- a/Source/Client/Comp/Map/ExposeActor.cs +++ b/Source/Client/Comp/Map/ExposeActor.cs @@ -21,7 +21,7 @@ public void ExposeData() // This depends on the fact that the implementation of HashSet RimWorld currently uses // "preserves" insertion order (as long as elements are only added and not removed // [which is the case for Scribe managers]) - public static void Register(Action action) + public static void OnPostInit(Action action) { if (Scribe.mode == LoadSaveMode.LoadingVars) Scribe.loader.initer.RegisterForPostLoadInit(new ExposeActor(action)); diff --git a/Source/Client/Comp/Map/FactionMapData.cs b/Source/Client/Comp/Map/FactionMapData.cs index dd862593..c5934a61 100644 --- a/Source/Client/Comp/Map/FactionMapData.cs +++ b/Source/Client/Comp/Map/FactionMapData.cs @@ -1,3 +1,4 @@ +using Multiplayer.Client.Factions; using RimWorld; using Verse; @@ -46,14 +47,14 @@ private FactionMapData(int factionId, Map map) : this(map) public void ExposeData() { - ExposeActor.Register(() => map.PushFaction(factionId)); + ExposeActor.OnPostInit(() => map.PushFaction(factionId)); Scribe_Values.Look(ref factionId, "factionId"); Scribe_Deep.Look(ref designationManager, "designationManager", map); Scribe_Deep.Look(ref areaManager, "areaManager", map); Scribe_Deep.Look(ref zoneManager, "zoneManager", map); - ExposeActor.Register(() => map.PopFaction()); + ExposeActor.OnPostInit(() => map.PopFaction()); } public static FactionMapData New(int factionId, Map map) diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 8c650718..1ebdbd80 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using HarmonyLib; +using Multiplayer.Client.Factions; using Multiplayer.Client.Persistent; using Multiplayer.Client.Saving; using Multiplayer.Common; using RimWorld; using RimWorld.Planet; +using UnityEngine; using Verse; namespace Multiplayer.Client @@ -17,14 +20,13 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Map map; - public IdBlock mapIdBlock; - public Dictionary factionData = new Dictionary(); - public Dictionary customFactionData = new Dictionary(); + public Dictionary factionData = new(); + public Dictionary customFactionData = new(); public CaravanFormingSession caravanForming; public TransporterLoading transporterLoading; public RitualSession ritualSession; - public List mapDialogs = new List(); + public List mapDialogs = new(); public int autosaveCounter; // for SaveCompression @@ -94,6 +96,13 @@ public void SetFaction(Faction faction) map.listerMergeables = data.listerMergeables; } + [Conditional("DEBUG")] + public void CheckInvariant() + { + if (factionData.TryGetValue(Faction.OfPlayer.loadID, out var data) && map.areaManager != data.areaManager) + Log.Error($"(Debug) Invariant broken for {Faction.OfPlayer}: {FactionContext.stack.ToStringSafeEnumerable()} {factionData.FirstOrDefault(d => d.Value.areaManager == map.areaManager)} {StackTraceUtility.ExtractStackTrace()}"); + } + public CustomFactionMapData GetCurrentCustomFactionData() { return customFactionData[Faction.OfPlayer.loadID]; @@ -121,11 +130,6 @@ public void ExposeData() if (Scribe.mode == LoadSaveMode.LoadingVars && mapDialogs == null) mapDialogs = new List(); - // todo for split sim - // Scribe_Custom.LookIdBlock(ref mapIdBlock, "mapIdBlock"); - // const int mapBlockSize = int.MaxValue / 2 / 1024; - // mapIdBlock ??= new IdBlock(int.MaxValue / 2 + mapBlockSize * map.uniqueID, mapBlockSize); - ExposeFactionData(); ExposeCustomFactionData(); } @@ -216,7 +220,9 @@ static void Prefix() // Trading window on resume save if (Multiplayer.WorldComp.trading.NullOrEmpty()) return; - if (Multiplayer.WorldComp.trading.FirstOrDefault(t => t.playerNegotiator == null) is MpTradeSession trade) + + // playerNegotiator == null can only happen during loading? Is this a resuming check? + if (Multiplayer.WorldComp.trading.FirstOrDefault(t => t.playerNegotiator == null) is { } trade) { trade.OpenWindow(); } diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index e7ef795e..54a986d2 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -17,6 +17,8 @@ public class MultiplayerWorldComp public List trading = new(); public CaravanSplittingSession splitSession; + public Faction spectatorFaction; + private int currentFactionId; public MultiplayerWorldComp(World world) @@ -25,14 +27,23 @@ public MultiplayerWorldComp(World world) uiTemperatures = new TileTemperaturesComp(world); } - // Called from AsyncWorldTimeComp.ExposeData + // Called from AsyncWorldTimeComp.ExposeData (for backcompat) public void ExposeData() { ExposeFactionData(); + Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); Scribe_Collections.Look(ref trading, "tradingSessions", LookMode.Deep); + if (Scribe.mode == LoadSaveMode.PostLoadInit) { + if (spectatorFaction == null) + { + spectatorFaction = HostUtil.AddNewFaction("Spectator", FactionDefOf.PlayerColony); + foreach (var map in Find.Maps) + MapSetup.InitNewFactionData(map, spectatorFaction); + } + if (trading.RemoveAll(t => t.trader == null || t.playerNegotiator == null) > 0) Log.Message("Some trading sessions had null entries"); } diff --git a/Source/Client/ConstantTicker.cs b/Source/Client/ConstantTicker.cs index 1928a138..4dd589b2 100644 --- a/Source/Client/ConstantTicker.cs +++ b/Source/Client/ConstantTicker.cs @@ -1,5 +1,6 @@ using System; using HarmonyLib; +using Multiplayer.Client.Factions; using Multiplayer.Common; using RimWorld; using Verse; @@ -37,7 +38,7 @@ public static void Tick() private const float TicksPerMinute = 60 * 60; private const float TicksPerIngameDay = 2500 * 24; - static void TickAutosave() + private static void TickAutosave() { if (Multiplayer.LocalServer is not { } server) return; @@ -66,7 +67,7 @@ static void TickAutosave() } } - static void TickSyncCoordinator() + private static void TickSyncCoordinator() { var sync = Multiplayer.game.sync; if (sync.ShouldCollect && TickPatch.Timer % 30 == 0 && sync.currentOpinion != null) @@ -115,7 +116,7 @@ public static void TickResearch() if (factionData.researchManager.currentProj == null) continue; - Extensions.PushFaction(null, factionData.factionId); + FactionExtensions.PushFaction(null, factionData.factionId); foreach (var kv in factionData.researchSpeed.data) { @@ -131,7 +132,7 @@ public static void TickResearch() dummyPawn.factionInt = null; } - Extensions.PopFaction(); + FactionExtensions.PopFaction(); } } } diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index 21673987..b99b055d 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -122,22 +122,6 @@ public static void SpawnShuttleAcceptColonists() GenPlace.TryPlaceThing(shuttle, UI.MouseCell(), Find.CurrentMap, ThingPlaceMode.Near); } - [DebugAction(MultiplayerCategory, "Save Map", allowedGameStates = AllowedGameStates.Playing)] - public static void SaveGameCmd() - { - Map map = Find.CurrentMap; - byte[] mapData = ScribeUtil.WriteExposable(Current.Game, "map", true); - File.WriteAllBytes($"map_{map.uniqueID}_{Multiplayer.username}.xml", mapData); - } - - [DebugAction(MultiplayerCategory, "Save Map (local)", allowedGameStates = AllowedGameStates.Playing)] - public static void SaveGameCmdLocal() - { - Map map = Find.CurrentMap; - byte[] mapData = ScribeUtil.WriteExposable(Current.Game, "map", true); - File.WriteAllBytes($"map_{map.uniqueID}_{Multiplayer.username}.xml", mapData); - } - [DebugAction(MultiplayerCategory, "Save Game", allowedGameStates = AllowedGameStates.Playing)] public static void SaveGame() { @@ -146,14 +130,6 @@ public static void SaveGame() File.WriteAllBytes($"game_{Multiplayer.username}.xml", data); } - [DebugAction(MultiplayerCategory, "Save Game (local)", allowedGameStates = AllowedGameStates.Playing)] - public static void SaveGameLocal() - { - Game game = Current.Game; - byte[] data = ScribeUtil.WriteExposable(game, "game", true); - File.WriteAllBytes($"game_{Multiplayer.username}.xml", data); - } - [DebugAction(MultiplayerCategory, "Dump Sync Types", allowedGameStates = AllowedGameStates.Entry)] public static void DumpSyncTypes() { diff --git a/Source/Client/Debug/DebugPatches.cs b/Source/Client/Debug/DebugPatches.cs index e50561c7..6b76bc2c 100644 --- a/Source/Client/Debug/DebugPatches.cs +++ b/Source/Client/Debug/DebugPatches.cs @@ -177,8 +177,8 @@ static void Prefix(ref string text) { // On Windows, Debug.Log used by Verse.Log replaces \n with \r\n // Without this patch printing \r\n results in \r\r\n - if (Native.Windows) - text = text?.Replace("\r\n", "\n"); + // if (Native.Windows) + // text = text?.Replace("\r\n", "\n"); } } diff --git a/Source/Client/Debug/DebugTools.cs b/Source/Client/Debug/DebugTools.cs index 88e88168..967d93ab 100644 --- a/Source/Client/Debug/DebugTools.cs +++ b/Source/Client/Debug/DebugTools.cs @@ -65,7 +65,7 @@ public static void HandleCmd(ByteReader data) { if (source == DebugSource.Tree) { - var node = GetNode(path); + var node = RecreateGraphAndGetNode(path); if (node != null) { node.Enter(null); @@ -150,9 +150,13 @@ public static void SendCmd(DebugSource source, int hash, string path, Map map) } // From Dialog_Debug.GetNode - public static DebugActionNode GetNode(string path) + public static DebugActionNode RecreateGraphAndGetNode(string path) { + // Some actions (like quest generation) invoke the RNG during global graph caching + // so we it recreate to avoid desyncs + Dialog_Debug.ResetStaticData(); Dialog_Debug.TrySetupNodeGraph(); + DebugActionNode curNode = Dialog_Debug.rootNode; string[] pathParts = path.Split('\\'); for (int i = 0; i < pathParts.Length; i++) diff --git a/Source/Client/Desyncs/DeferredStackTracing.cs b/Source/Client/Desyncs/DeferredStackTracing.cs index a5437c70..105eb056 100644 --- a/Source/Client/Desyncs/DeferredStackTracing.cs +++ b/Source/Client/Desyncs/DeferredStackTracing.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; using System.Reflection; -using System.Runtime.CompilerServices; using HarmonyLib; using Multiplayer.Client.Patches; using RimWorld; @@ -11,7 +9,7 @@ namespace Multiplayer.Client.Desyncs { [EarlyPatch] [HarmonyPatch] - static class DeferredStackTracing + public static class DeferredStackTracing { public static int ignoreTraces; public static long maxTraceDepth; @@ -31,14 +29,14 @@ public static void Postfix() if (Native.LmfPtr == 0) return; if (!ShouldAddStackTraceForDesyncLog()) return; - var logItem = SimplePool.Get(); + var logItem = StackTraceLogItemRaw.GetFromPool(); var trace = logItem.raw; int hash = 0; int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash); Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash); - acc = Gen.HashCombineInt(acc, depth, hash, (int)Rand.iterations); + acc++; } public static bool ShouldAddStackTraceForDesyncLog() @@ -59,244 +57,6 @@ public static bool ShouldAddStackTraceForDesyncLog() } } - static class DeferredStackTracingImpl - { - struct AddrInfo - { - public long addr; - public long stackUsage; - public long nameHash; - public long unused; - } - - const int StartingN = 7; - const int StartingShift = 64 - StartingN; - const int StartingSize = 1 << StartingN; - const float LoadFactor = 0.5f; - - static AddrInfo[] hashtable = new AddrInfo[StartingSize]; - public static int hashtableSize = StartingSize; - public static int hashtableEntries; - public static int hashtableShift = StartingShift; - public static int collisions; - - const long NotJIT = long.MaxValue; - const long RBPBased = long.MaxValue - 1; - - const long UsesRBPAsGPR = 1 << 50; - const long UsesRBX = 1 << 51; - const long RBPInfoClearMask = ~(UsesRBPAsGPR | UsesRBX); - - public const int MaxDepth = 32; - public const int HashInfluence = 6; - - public unsafe static int TraceImpl(long[] traceIn, ref int hash) - { - long[] trace = traceIn; - long rbp = GetRbp(); - long stck = rbp; - rbp = *(long*)rbp; - - int indexmask = hashtableSize - 1; - int shift = hashtableShift; - - long ret; - long lmfPtr = *(long*)Native.LmfPtr; - - int depth = 0; - - while (true) - { - ret = *(long*)(stck + 8); - - int index = (int)(HashAddr((ulong)ret) >> shift); - ref var info = ref hashtable[index]; - int colls = 0; - - // Open addressing - while (info.addr != 0 && info.addr != ret) - { - index = (index + 1) & indexmask; - info = ref hashtable[index]; - colls++; - } - - if (colls > collisions) - collisions = colls; - - long stackUsage = 0; - - if (info.addr != 0) - stackUsage = info.stackUsage; - else - stackUsage = UpdateNewElement(ref info, ret); - - if (stackUsage == NotJIT) - { - // LMF (Last Managed Frame) layout on x64: - // previous - // rbp - // rsp - - lmfPtr = *(long*)lmfPtr; - var lmfRbp = *(long*)(lmfPtr + 8); - - if (lmfPtr == 0 || lmfRbp == 0) - break; - - rbp = lmfRbp; - stck = *(long*)(lmfPtr + 16) - 16; - - continue; - } - - trace[depth] = ret; - - if (depth < HashInfluence) - hash = Gen.HashCombineInt(hash, (int)info.nameHash); - - if (++depth == MaxDepth) - break; - - if (stackUsage == RBPBased) - { - stck = rbp; - rbp = *(long*)rbp; - continue; - } - - stck += 8; - - if ((stackUsage & UsesRBPAsGPR) != 0) - { - if ((stackUsage & UsesRBX) != 0) - rbp = *(long*)(stck + 16); - else - rbp = *(long*)(stck + 8); - - stackUsage &= RBPInfoClearMask; - } - - stck += stackUsage; - } - - return depth; - } - - static long UpdateNewElement(ref AddrInfo info, long ret) - { - long stackUsage = GetStackUsage(ret); - - info.addr = ret; - info.stackUsage = stackUsage; - - var rawName = Native.MethodNameFromAddr(ret, true); - info.nameHash = rawName != null ? GenText.StableStringHash(SyncCoordinator.MethodNameWithoutIL(rawName)) : 1; - - hashtableEntries++; - if (hashtableEntries > hashtableSize * LoadFactor) - ResizeHashtable(); - - return stackUsage; - } - - static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485; - - static int ResizeHashtable() - { - var oldTable = hashtable; - - hashtableSize *= 2; - hashtableShift--; - - hashtable = new AddrInfo[hashtableSize]; - collisions = 0; - - int indexmask = hashtableSize - 1; - int shift = hashtableShift; - - for (int i = 0; i < oldTable.Length; i++) - { - ref var oldInfo = ref oldTable[i]; - if (oldInfo.addr != 0) - { - int index = (int)(HashAddr((ulong)oldInfo.addr) >> shift); - - while (hashtable[index].addr != 0) - index = (index + 1) & indexmask; - - ref var newInfo = ref hashtable[index]; - newInfo.addr = oldInfo.addr; - newInfo.stackUsage = oldInfo.stackUsage; - newInfo.nameHash = oldInfo.nameHash; - } - } - - return indexmask; - } - - unsafe static long GetStackUsage(long addr) - { - var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); - - if (ji == IntPtr.Zero) - return NotJIT; - - var start = (uint*)Native.mono_jit_info_get_code_start(ji); - long usage = 0; - - if ((*start & 0xFFFFFF) == 0xEC8348) // sub rsp,XX (4883EC XX) - { - usage = *start >> 24; - start += 1; - } else if ((*start & 0xFFFFFF) == 0xEC8148) // sub rsp,XXXXXXXX (4881EC XXXXXXXX) - { - usage = *(uint*)((long)start + 3); - start = (uint*)((long)start + 7); - } - - if (usage != 0) - { - CheckRbpUsage(start, ref usage); - return usage; - } - - // push rbp (55) - if (*(byte*)start == 0x55) - return RBPBased; - - throw new Exception($"Deferred stack tracing: Unknown function header {*start} {Native.MethodNameFromAddr(addr, false)}"); - } - - private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage) - { - // If rbp is used as a gp reg then the prologue looks like (after frame alloc): - // mov [rsp],rbp (48892C24) - // or: - // mov [rsp],rbx (48891C24) - // mov [rsp+8],rbp (48896C2408) - // (The calle saved registers are always in the same order - // and are saved at the bottom of the frame) - - if (*at == 0x242C8948) - { - stackUsage |= UsesRBPAsGPR; - } - else if (*at == 0x241C8948 && *(at + 1) == 0x246C8948) - { - stackUsage |= UsesRBPAsGPR; - stackUsage |= UsesRBX; - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - static unsafe long GetRbp() - { - long rbp = 0; - return *(&rbp + 1); - } - } - [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] static class WildAnimalSpawnerTickTraceIgnore { diff --git a/Source/Client/Desyncs/StackTraceLogItem.cs b/Source/Client/Desyncs/StackTraceLogItem.cs index d210bd26..f4c0798c 100644 --- a/Source/Client/Desyncs/StackTraceLogItem.cs +++ b/Source/Client/Desyncs/StackTraceLogItem.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Text; using Multiplayer.Client.Desyncs; using Verse; @@ -38,15 +37,16 @@ public class StackTraceLogItemRaw : StackTraceLogItem public long[] raw = new long[DeferredStackTracingImpl.MaxDepth]; public int depth; + public int ticksGame; public int iters; public ThingDef thingDef; public int thingId; public string factionName; public string moreInfo; - public override string AdditionalInfo => $"{thingDef}{thingId} {factionName} {depth} {iters} {moreInfo}"; + public override string AdditionalInfo => $"{ticksGame} {thingDef}{thingId} {factionName} {depth} {iters} {moreInfo}"; - static Dictionary methodNames = new(); + private static Dictionary methodNameCache = new(); public override string StackTraceString { @@ -57,8 +57,8 @@ public override string StackTraceString { var addr = raw[i]; - if (!methodNames.TryGetValue(addr, out string method)) - methodNames[addr] = method = Native.MethodNameFromAddr(raw[i], false); + if (!methodNameCache.TryGetValue(addr, out string method)) + methodNameCache[addr] = method = Native.MethodNameFromAddr(raw[i], false); if (method != null) builder.AppendLine(SyncCoordinator.MethodNameWithIL(method)); @@ -70,9 +70,15 @@ public override string StackTraceString } } + public static StackTraceLogItemRaw GetFromPool() + { + return SimplePool.Get(); + } + public override void ReturnToPool() { depth = 0; + ticksGame = 0; iters = 0; thingId = 0; thingDef = null; diff --git a/Source/Client/Desyncs/SyncCoordinator.cs b/Source/Client/Desyncs/SyncCoordinator.cs index 683f7396..33906001 100644 --- a/Source/Client/Desyncs/SyncCoordinator.cs +++ b/Source/Client/Desyncs/SyncCoordinator.cs @@ -1,7 +1,6 @@ using Multiplayer.Common; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using RimWorld; using Verse; @@ -10,7 +9,6 @@ namespace Multiplayer.Client { - public class SyncCoordinator { public bool ShouldCollect => !Multiplayer.IsReplay; @@ -218,6 +216,7 @@ public void TryAddStackTraceForDesyncLogRaw(StackTraceLogItemRaw item, int depth OpinionInBuilding.TryMarkSimulating(); item.depth = depth; + item.ticksGame = Find.TickManager.ticksGameInt; item.iters = (int)Rand.iterations; item.tick = TickPatch.Timer; item.factionName = Faction.OfPlayer?.Name ?? string.Empty; diff --git a/Source/Client/EarlyInit.cs b/Source/Client/EarlyInit.cs index eeba3c65..a97839d0 100644 --- a/Source/Client/EarlyInit.cs +++ b/Source/Client/EarlyInit.cs @@ -66,18 +66,18 @@ internal static void InitSync() internal static void LatePatches(Harmony harmony) { // optimization, cache DescendantThingDefs - harmony.PatchMeasure( - AccessTools.Method(typeof(ThingCategoryDef), "get_DescendantThingDefs"), - new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Prefix"), - new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Postfix") - ); + // harmony.PatchMeasure( + // AccessTools.Method(typeof(ThingCategoryDef), "get_DescendantThingDefs"), + // new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Prefix"), + // new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Postfix") + // ); // optimization, cache ThisAndChildCategoryDefs - harmony.PatchMeasure( - AccessTools.Method(typeof(ThingCategoryDef), "get_ThisAndChildCategoryDefs"), - new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Prefix"), - new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Postfix") - ); + // harmony.PatchMeasure( + // AccessTools.Method(typeof(ThingCategoryDef), "get_ThisAndChildCategoryDefs"), + // new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Prefix"), + // new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Postfix") + // ); if (MpVersion.IsDebug) { diff --git a/Source/Client/Factions/AutoRoofFactionPatch.cs b/Source/Client/Factions/AutoRoofFactionPatch.cs new file mode 100644 index 00000000..2ac5783c --- /dev/null +++ b/Source/Client/Factions/AutoRoofFactionPatch.cs @@ -0,0 +1,41 @@ +using HarmonyLib; +using Multiplayer.Client.Factions; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +[HarmonyPatch(typeof(AutoBuildRoofAreaSetter))] +[HarmonyPatch(nameof(AutoBuildRoofAreaSetter.TryGenerateAreaNow))] +public static class AutoRoofFactionPatch +{ + static bool Prefix(AutoBuildRoofAreaSetter __instance, Room room, ref Map __state) + { + if (Multiplayer.Client == null) return true; + if (room.Dereferenced || room.TouchesMapEdge || room.RegionCount > 26 || room.CellCount > 320 || room.IsDoorway) return false; + + Map map = room.Map; + Faction faction = null; + + foreach (IntVec3 cell in room.BorderCells) + { + Thing holder = cell.GetRoofHolderOrImpassable(map); + if (holder == null || holder.Faction == null) continue; + if (faction != null && holder.Faction != faction) return false; + faction = holder.Faction; + } + + if (faction == null) return false; + + map.PushFaction(faction); + __state = map; + + return true; + } + + static void Postfix(ref Map __state) + { + if (__state != null) + __state.PopFaction(); + } +} diff --git a/Source/Client/Factions/FactionContext.cs b/Source/Client/Factions/FactionContext.cs index 0c525313..1697d147 100644 --- a/Source/Client/Factions/FactionContext.cs +++ b/Source/Client/Factions/FactionContext.cs @@ -6,11 +6,11 @@ namespace Multiplayer.Client { public static class FactionContext { - public static Stack stack = new Stack(); + public static Stack stack = new(); public static Faction Push(Faction newFaction) { - if (newFaction == null || !newFaction.def.isPlayer) + if (newFaction == null || Find.FactionManager.ofPlayer == newFaction || !newFaction.def.isPlayer) { stack.Push(null); return null; diff --git a/Source/Client/Factions/FactionCreator.cs b/Source/Client/Factions/FactionCreator.cs new file mode 100644 index 00000000..48d70c2d --- /dev/null +++ b/Source/Client/Factions/FactionCreator.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using Multiplayer.API; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client.Factions; + +public static class FactionCreator +{ + private static Dictionary> pawnStore = new(); + + public static void ClearData() + { + pawnStore.Clear(); + } + + [SyncMethod(exposeParameters = new[] { 1 })] + public static void SendPawn(int sessionId, Pawn p) + { + pawnStore.GetOrAddNew(sessionId).Add(p); + } + + [SyncMethod] + public static void CreateFaction(int sessionId, string factionName, int tile, string scenario, FactionRelationKind relation) + { + PrepareState(sessionId); + + var self = TickPatch.currentExecutingCmdIssuedBySelf; + + LongEventHandler.QueueLongEvent(delegate + { + int id = Find.UniqueIDsManager.GetNextFactionID(); + var newFaction = NewFaction(id, factionName, FactionDefOf.PlayerColony); + + newFaction.hidden = true; + + foreach (var f in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) + if (f != newFaction) + { + newFaction.SetRelation(new FactionRelation(f, relation)); + } + + FactionContext.Push(newFaction); + var newMap = GenerateNewMap(tile, scenario); + FactionContext.Pop(); + + // Add new faction to all maps but the new + foreach (Map map in Find.Maps) + if (map != newMap) + MapSetup.InitNewFactionData(map, newFaction); + + foreach (Map map in Find.Maps) + foreach (var f in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) + map.attackTargetsCache.Notify_FactionHostilityChanged(f, newFaction); + + FactionContext.Push(newFaction); + try + { + InitNewGame(); + } + finally + { + FactionContext.Pop(); + } + + if (self) + { + Current.Game.CurrentMap = newMap; + + Multiplayer.game.ChangeRealPlayerFaction(newFaction); + + // todo setting faction of self + Multiplayer.Client.Send( + Packets.Client_SetFaction, + Multiplayer.session.playerId, + newFaction.loadID + ); + } + }, "GeneratingMap", doAsynchronously: true, GameAndMapInitExceptionHandlers.ErrorWhileGeneratingMap); + } + + private static Map GenerateNewMap(int tile, string scenario) + { + // This has to be null, otherwise, during map generation, Faction.OfPlayer returns it which breaks FactionContext + Find.GameInitData.playerFaction = null; + Find.GameInitData.PrepForMapGen(); + + var mapParent = (Settlement)WorldObjectMaker.MakeWorldObject(WorldObjectDefOf.Settlement); + mapParent.Tile = tile; + mapParent.SetFaction(Faction.OfPlayer); + Find.WorldObjects.Add(mapParent); + + var prevScenario = Find.Scenario; + Current.Game.scenarioInt = ScenarioLister.AllScenarios().First(s => s.name == scenario); + + try + { + return GetOrGenerateMapUtility.GetOrGenerateMap( + tile, + new IntVec3(250, 1, 250), + null + ); + } + finally + { + Current.Game.scenarioInt = prevScenario; + } + } + + private static void InitNewGame() + { + PawnUtility.GiveAllStartingPlayerPawnsThought(ThoughtDefOf.NewColonyOptimism); + ResearchUtility.ApplyPlayerStartingResearch(); + } + + public static void SetInitialInitData() + { + Current.Game.InitData = new GameInitData + { + startingPawnCount = 3, + gameToLoad = "dummy" // Prevent special calculation path in GenTicks.TicksAbs + }; + } + + public static void PrepareState(int sessionId) + { + SetInitialInitData(); + + if (pawnStore.TryGetValue(sessionId, out var pawns)) + { + Current.Game.InitData.startingAndOptionalPawns = pawns; + Current.Game.InitData.startingPossessions = new Dictionary>(); + foreach (var p in pawns) + Current.Game.InitData.startingPossessions[p] = new List(); + + pawnStore.Remove(sessionId); + } + } + + private static Faction NewFaction(int id, string name, FactionDef def) + { + Faction faction = Find.FactionManager.AllFactions.FirstOrDefault(f => f.loadID == id); + + if (faction == null) + { + faction = new Faction() { loadID = id, def = def }; + + faction.ideos = new FactionIdeosTracker(faction); + faction.ideos.ChooseOrGenerateIdeo(new IdeoGenerationParms()); + + foreach (Faction other in Find.FactionManager.AllFactionsListForReading) + faction.TryMakeInitialRelationsWith(other); + + Find.FactionManager.Add(faction); + + Multiplayer.WorldComp.factionData[faction.loadID] = + FactionWorldData.New(faction.loadID); + } + + faction.Name = name; + faction.def = def; + + return faction; + } +} diff --git a/Source/Client/Factions/FactionExtensions.cs b/Source/Client/Factions/FactionExtensions.cs new file mode 100644 index 00000000..a971cd47 --- /dev/null +++ b/Source/Client/Factions/FactionExtensions.cs @@ -0,0 +1,43 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Factions; + +public static class FactionExtensions +{ + // Sets the current Faction.OfPlayer + // Applies faction's world components + // Applies faction's map components if map not null + public static void PushFaction(this Map map, Faction f) + { + map?.MpComp()?.CheckInvariant(); + + var faction = FactionContext.Push(f); + if (faction == null) return; + + Multiplayer.WorldComp?.SetFaction(faction); + map?.MpComp().SetFaction(faction); + } + + public static void PushFaction(this Map map, int factionId) + { + Faction faction = Find.FactionManager.GetById(factionId); + map.PushFaction(faction); + } + + public static void PopFaction() + { + PopFaction(null); + } + + public static Faction PopFaction(this Map map) + { + Faction faction = FactionContext.Pop(); + if (faction == null) return null; + + Multiplayer.WorldComp?.SetFaction(faction); + map?.MpComp().SetFaction(faction); + + return faction; + } +} diff --git a/Source/Client/Factions/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs index c23e38ab..01540e8b 100644 --- a/Source/Client/Factions/FactionRepeater.cs +++ b/Source/Client/Factions/FactionRepeater.cs @@ -1,5 +1,6 @@ using System; using HarmonyLib; +using Multiplayer.Client.Factions; using RimWorld; using Verse; @@ -68,7 +69,7 @@ static class ListerMergeablesSpawnedPatch static bool ignore; static bool Prefix(ListerMergeables __instance, Thing t) => - FactionRepeater.Template(d => d.listerMergeables.Notify_DeSpawned(t), __instance.map, ref ignore); + FactionRepeater.Template(d => d.listerMergeables.Notify_Spawned(t), __instance.map, ref ignore); } [HarmonyPatch(typeof(ListerMergeables), nameof(ListerMergeables.Notify_DeSpawned))] diff --git a/Source/Client/Factions/FactionSidebar.cs b/Source/Client/Factions/FactionSidebar.cs new file mode 100644 index 00000000..233e195c --- /dev/null +++ b/Source/Client/Factions/FactionSidebar.cs @@ -0,0 +1,174 @@ +using System.Linq; +using System.Text; +using Multiplayer.Client.Factions; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public class FactionSidebar +{ + private static string scenario = "Crashlanded"; + private static string factionName; + private static FactionRelationKind hostility = FactionRelationKind.Neutral; + private static Vector2 scroll; + + public static void DrawFactionSidebar(Rect rect) + { + using var _ = MpStyle.Set(GameFont.Small); + + if (!Layouter.BeginArea(rect)) + return; + + Layouter.BeginScroll(ref scroll, spacing: 0f); + + using (MpStyle.Set(TextAnchor.MiddleLeft)) + using (MpStyle.Set(GameFont.Medium)) + Label("Create faction"); + + DrawFactionCreator(); + + Layouter.Rect(0, 20); + + using (MpStyle.Set(Color.gray)) + Widgets.DrawLineHorizontal(Layouter.LastRect().x, Layouter.LastRect().center.y, rect.width); + + using (MpStyle.Set(TextAnchor.MiddleLeft)) + using (MpStyle.Set(GameFont.Medium)) + Label("Join faction"); + + DrawFactionChooser(); + + Layouter.EndScroll(); + Layouter.EndArea(); + } + + private static void DrawFactionCreator() + { + factionName = Widgets.TextField(Layouter.Rect(150, 24), factionName); + + if (Button("Settle new faction", 130)) + { + var tileError = new StringBuilder(); + + // todo check faction name not exists + if (factionName.NullOrEmpty()) + Messages.Message("The faction name can't be empty.", MessageTypeDefOf.RejectInput, historical: false); + else if (Find.WorldInterface.SelectedTile < 0) + Messages.Message("MustSelectStartingSite".TranslateWithBackup("MustSelectLandingSite"), MessageTypeDefOf.RejectInput, historical: false); + else if (!TileFinder.IsValidTileForNewSettlement(Find.WorldInterface.SelectedTile, tileError)) + Messages.Message(tileError.ToString(), MessageTypeDefOf.RejectInput, historical: false); + else + { + PreparePawns(); + + Find.WindowStack.Add(new Page_ConfigureStartingPawns + { + nextAct = DoCreateFaction + }); + } + } + } + + private static void PreparePawns() + { + var prevState = Current.programStateInt; + Current.programStateInt = ProgramState.Entry; // Set ProgramState.Entry so that InInterface is false + + try + { + FactionCreator.SetInitialInitData(); + + // Create starting pawns + new ScenPart_ConfigPage_ConfigureStartingPawns { pawnCount = Current.Game.InitData.startingPawnCount } + .GenerateStartingPawns(); + } + finally + { + Current.programStateInt = prevState; + } + } + + private static void DoCreateFaction() + { + int sessionId = Multiplayer.session.playerId; + var prevState = Current.programStateInt; + Current.programStateInt = ProgramState.Playing; // This is to force a sync + + try + { + foreach (var p in Current.Game.InitData.startingAndOptionalPawns) + FactionCreator.SendPawn( + sessionId, + p + ); + + FactionCreator.CreateFaction(sessionId, factionName, Find.WorldInterface.SelectedTile, + scenario, hostility); + } + finally + { + Current.programStateInt = prevState; + } + } + + private static void DrawFactionChooser() + { + int i = 0; + + foreach (var playerFaction in Find.FactionManager.AllFactions.Where(f => f.def == FactionDefOf.PlayerColony)) + { + if (playerFaction.Name == "Spectator") continue; + + Layouter.BeginHorizontal(); + if (i % 2 == 0) + Widgets.DrawAltRect(Layouter.GroupRect()); + + if (Mouse.IsOver(Layouter.GroupRect())) + { + // todo this doesn't exactly work + foreach (var settlement in Find.WorldObjects.Settlements) + if (settlement.Faction == playerFaction) + Graphics.DrawMesh(MeshPool.plane20, + Find.WorldGrid.GetTileCenter(settlement.Tile), + Quaternion.identity, + GenDraw.ArrowMatWhite, + 0); + + Widgets.DrawRectFast(Layouter.GroupRect(), new Color(0.2f, 0.2f, 0.2f)); + } + + using (MpStyle.Set(TextAnchor.MiddleCenter)) + Label(playerFaction.Name, true); + + Layouter.FlexibleWidth(); + if (Button("Join", 70)) + { + Current.Game.CurrentMap = Find.Maps.First(m => m.ParentFaction == playerFaction); + + // todo setting faction of self + Multiplayer.Client.Send( + Packets.Client_SetFaction, + Multiplayer.session.playerId, + playerFaction.loadID + ); + } + Layouter.EndHorizontal(); + i++; + } + } + + public static void Label(string text, bool inheritHeight = false) + { + GUI.Label(inheritHeight ? Layouter.FlexibleWidth() : Layouter.ContentRect(text), text, Text.CurFontStyle); + } + + public static bool Button(string text, float width, float height = 35f) + { + return Widgets.ButtonText(Layouter.Rect(width, height), text); + } +} diff --git a/Source/Client/Patches/Forbiddables.cs b/Source/Client/Factions/Forbiddables.cs similarity index 100% rename from Source/Client/Patches/Forbiddables.cs rename to Source/Client/Factions/Forbiddables.cs diff --git a/Source/Client/Factions/Multifaction.cs b/Source/Client/Factions/Multifaction.cs deleted file mode 100644 index 42543a77..00000000 --- a/Source/Client/Factions/Multifaction.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using Multiplayer.API; -using RimWorld; -using RimWorld.Planet; -using Verse; - -namespace Multiplayer.Client.Patches; - -[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] -static class DisableDraftGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) - { - return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); - } -} - -[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] -static class PawnChangeRelationGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) - { - foreach (var gizmo in gizmos) - yield return gizmo; - - if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) - { - var otherFaction = __instance.Faction; - - yield return new Command_Action() - { - defaultLabel = "Change faction relation", - action = () => - { - List list = new List(); - for (int i = 0; i <= 2; i++) - { - var kind = (FactionRelationKind)i; - list.Add(new FloatMenuOption(kind.ToString(), () => { SetFactionRelation(otherFaction, kind); })); - } - - Find.WindowStack.Add(new FloatMenu(list)); - } - }; - } - } - - [SyncMethod] - static void SetFactionRelation(Faction other, FactionRelationKind kind) - { - Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); - } -} - -[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] -static class CheckDefeatedPatch -{ - static bool Prefix(Settlement factionBase) - { - return factionBase.Faction is not { IsPlayer: true }; - } -} - -[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] -static class CheckRemoveMapNowPatch -{ - static bool Prefix(MapParent __instance) - { - return __instance.Faction is not { IsPlayer: true }; - } -} - -// todo this is temporary -// [HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GoodwillManagerTick))] -// static class GoodwillManagerTickCancel -// { -// static bool Prefix() => false; -// } diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs new file mode 100644 index 00000000..dd836a38 --- /dev/null +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using HarmonyLib; +using Multiplayer.API; +using Multiplayer.Client.Util; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Patches; + +[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] +static class DisableDraftGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) + { + return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] +static class PawnChangeRelationGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) + { + foreach (var gizmo in gizmos) + yield return gizmo; + + if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) + { + var otherFaction = __instance.Faction; + + yield return new Command_Action() + { + defaultLabel = "Change faction relation", + icon = MultiplayerStatic.ChangeRelationIcon, + action = () => + { + List list = new List(); + for (int i = 0; i <= 2; i++) + { + var kind = (FactionRelationKind)i; + list.Add(new FloatMenuOption(kind.ToString(), () => { SetFactionRelation(otherFaction, kind); })); + } + + Find.WindowStack.Add(new FloatMenu(list)); + } + }; + } + } + + [SyncMethod] + static void SetFactionRelation(Faction other, FactionRelationKind kind) + { + Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); + } +} + +[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] +static class CheckDefeatedPatch +{ + static bool Prefix(Settlement factionBase) + { + return factionBase.Faction is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] +static class CheckRemoveMapNowPatch +{ + static bool Prefix(MapParent __instance) + { + return __instance.Faction is not { IsPlayer: true }; + } +} + +// todo this is temporary +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GoodwillManagerTick))] +static class GoodwillManagerTickCancel +{ + static bool Prefix() => false; +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] +static class SettlementAttackablePatch +{ + static bool Prefix() => false; // todo should only be player +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] +static class SettlementNullFactionPatch1 +{ + static bool Prefix(Settlement __instance, ref Material __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadMat; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] +static class SettlementNullFactionPatch2 +{ + static void OnCodeReload(int version) + { + var harmony = new Harmony("mptestpatches"); + harmony.UnpatchAll("mptestpatches"); + + static bool GoodwillPrefix(Faction other) + { + return other is not { IsPlayer: true }; + } + + static void PrintPrefix() + { + Log.Message("add pawn"); + } + + harmony.Patch( + MethodOf.Inner((GoodwillSituationManager m) => m.GetNaturalGoodwill), + MethodOf.Lambda(GoodwillPrefix).Harmony() + ); + + harmony.Patch( + MethodOf.Inner((GoodwillSituationManager m) => m.GetMaxGoodwill), + MethodOf.Lambda(GoodwillPrefix).Harmony() + ); + + harmony.Patch( + MethodOf.Inner((WorldPawns w) => w.AddPawn(null)), + MethodOf.Lambda(PrintPrefix).Harmony() + ); + } + + static bool Prefix(Settlement __instance, ref Texture2D __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadTex; + return false; + } + + return true; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetNaturalGoodwill))] +static class GetNaturalGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GetMaxGoodwill))] +static class GetMaxGoodwillPatch +{ + static bool Prefix(Faction other) + { + return other is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(ScenPart_StartingAnimal), nameof(ScenPart_StartingAnimal.PetWeight))] +static class StartingAnimalPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var playerFactionField = AccessTools.Field(typeof(GameInitData), nameof(GameInitData.playerFaction)); + var factionOfPlayer = FactionOfPlayer; + + foreach (var inst in insts) + { + yield return inst; + + if (inst.operand == playerFactionField) + yield return new CodeInstruction(OpCodes.Call, factionOfPlayer.Method); + } + } + + static Faction FactionOfPlayer(Faction f) + { + return Faction.OfPlayer; + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.IsColonist), MethodType.Getter)] +static class PawnIsColonistPatch +{ + static IEnumerable Transpiler(IEnumerable insts) + { + var isPlayerMethodGetter = AccessTools.PropertyGetter(typeof(Faction), nameof(Faction.IsPlayer)); + var factionIsPlayer = FactionIsPlayer; + + foreach (var inst in insts) + { + if (inst.operand == isPlayerMethodGetter) + inst.operand = factionIsPlayer.Method; + + yield return inst; + } + } + + static bool FactionIsPlayer(Faction f) + { + return f == Find.FactionManager.OfPlayer; + } +} + +[HarmonyPatch(typeof(GetOrGenerateMapUtility), nameof(GetOrGenerateMapUtility.GetOrGenerateMap), new []{ typeof(int), typeof(IntVec3), typeof(WorldObjectDef) })] +static class MapGenFactionPatch +{ + static void Prefix(int tile) + { + var mapParent = Find.WorldObjects.MapParentAt(tile); + if (Multiplayer.Client != null && mapParent == null) + Log.Warning($"Couldn't set the faction context for map gen at {tile}: no world object"); + + FactionContext.Push(mapParent?.Faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(CaravanEnterMapUtility), nameof(CaravanEnterMapUtility.Enter), new[] { typeof(Caravan), typeof(Map), typeof(Func), typeof(CaravanDropInventoryMode), typeof(bool) })] +static class CaravanEnterFactionPatch +{ + static void Prefix(Caravan caravan) + { + FactionContext.Push(caravan.Faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(WealthWatcher), nameof(WealthWatcher.ForceRecount))] +static class WealthRecountFactionPatch +{ + static void Prefix(WealthWatcher __instance) + { + FactionContext.Push(__instance.map.ParentFaction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.DoWindowContents))] +static class ConfigureStartingPawns_DoWindowContents_Patch +{ + static void Prefix(ref ProgramState __state) + { + __state = Current.ProgramState; + Current.programStateInt = ProgramState.Entry; + } + + static void Finalizer(ProgramState __state) + { + Current.programStateInt = __state; + } +} + +[HarmonyPatch(typeof(Page_ConfigureStartingPawns), nameof(Page_ConfigureStartingPawns.RandomizeCurPawn))] +static class ConfigureStartingPawns_RandomizeCurPawn_Patch +{ + static void Prefix(ref ProgramState __state) + { + __state = Current.ProgramState; + Current.programStateInt = ProgramState.Entry; + } + + static void Finalizer(ProgramState __state) + { + Current.programStateInt = __state; + } +} + +[HarmonyPatch(typeof(LifeStageWorker_HumanlikeAdult), nameof(LifeStageWorker_HumanlikeAdult.Notify_LifeStageStarted))] +static class LifeStageWorker_Patch +{ + static bool Prefix() + { + // Corresponds to "Current.ProgramState == ProgramState.Playing" check in Notify_LifeStageStarted + return !ScribeUtil.loading; + } +} + +[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.GetGenerationRequest))] +static class StartingPawnUtility_GetGenerationRequest_Patch +{ + static void Postfix(ref PawnGenerationRequest __result) + { + if (Multiplayer.Client != null) + __result.CanGeneratePawnRelations = false; + } +} + +[HarmonyPatch(typeof(StartingPawnUtility), nameof(StartingPawnUtility.DefaultStartingPawnRequest), MethodType.Getter)] +static class StartingPawnUtility_DefaultStartingPawnRequest_Patch +{ + static void Postfix(ref PawnGenerationRequest __result) + { + if (Multiplayer.Client != null) + __result.CanGeneratePawnRelations = false; + } +} diff --git a/Source/Client/Factions/SidebarPatch.cs b/Source/Client/Factions/SidebarPatch.cs new file mode 100644 index 00000000..ff2e48f7 --- /dev/null +++ b/Source/Client/Factions/SidebarPatch.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Factions; + +[HarmonyPatch(typeof(UIRoot_Play), nameof(UIRoot_Play.UIRootOnGUI))] +static class UIRootPrefix +{ + static void Postfix() + { + // if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction != null && Find.FactionManager != null) + // Find.FactionManager.ofPlayer = Multiplayer.RealPlayerFaction; + + Layouter.BeginFrame(); + + if (Multiplayer.Client != null && + Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction && + !TickPatch.Simulating && + Find.WindowStack.WindowOfType() == null + ) + Find.WindowStack.ImmediateWindow( + "MpWindowFaction".GetHashCode(), + new Rect(0, UI.screenHeight / 2f - 400 / 2f, 300, 350), + WindowLayer.Super, + () => + { + FactionSidebar.DrawFactionSidebar(new Rect(0, 0, 300, 350).ContractedBy(15)); + } + ); + } +} diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 4c13f35d..37ecc15c 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -37,7 +37,6 @@ public static class Multiplayer public static bool reloading; - public static IdBlock GlobalIdBlock => game.gameComp.globalIdBlock; public static MultiplayerGameComp GameComp => game.gameComp; public static MultiplayerWorldComp WorldComp => game.worldComp; public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; @@ -75,7 +74,16 @@ public static class Multiplayer public static void InitMultiplayer() { - Native.EarlyInit(); + Native.EarlyInit( + Application.platform switch + { + RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, + RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, + RuntimePlatform.OSXEditor => Native.NativeOS.OSX, + RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, + _ => Native.NativeOS.Windows + }); + DisableOmitFramePointer(); MultiplayerLoader.Multiplayer.settingsWindowDrawer = @@ -187,6 +195,7 @@ public static void StopMultiplayer() { session.Stop(); session = null; + Prefs.Apply(); } diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index 694161fa..74e979f7 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -3,9 +3,9 @@ net472 true - 10 + 11 false - ..\..\AssembliesCustom\ + bin false false 0.6.2 @@ -29,8 +29,9 @@ - - + + + @@ -43,12 +44,13 @@ - - + + - + + @@ -58,4 +60,16 @@ + + + + + + + + + + + + diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index a633647e..15f9851d 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -6,14 +6,13 @@ using System.Reflection; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Client.Factions; using Multiplayer.Client.Persistent; -using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { - [HotSwappable] public class MultiplayerGame { public SyncCoordinator sync = new(); @@ -76,6 +75,8 @@ public MultiplayerGame() foreach (var initialOpinion in Multiplayer.session.initialOpinions) sync.AddClientOpinionAndCheckDesync(initialOpinion); Multiplayer.session.initialOpinions.Clear(); + + FactionCreator.ClearData(); } public static void ClearPortraits() @@ -138,6 +139,8 @@ public void ChangeRealPlayerFaction(int newFaction) public void ChangeRealPlayerFaction(Faction newFaction) { + Log.Message($"Changing real player faction to {newFaction} from {myFaction}"); + myFaction = newFaction; FactionContext.Set(newFaction); worldComp.SetFaction(newFaction); @@ -145,8 +148,16 @@ public void ChangeRealPlayerFaction(Faction newFaction) foreach (Map m in Find.Maps) m.MpComp().SetFaction(newFaction); + foreach (Map m in Find.Maps) + { + m.mapDrawer.RegenerateEverythingNow(); + + foreach (var t in m.listerThings.AllThings) + if (t is ThingWithComps tc) + tc.GetComp()?.UpdateOverlayHandle(); + } + Find.ColonistBar?.MarkColonistsDirty(); - Find.CurrentMap?.mapDrawer.RegenerateEverythingNow(); } } } diff --git a/Source/Client/MultiplayerSession.cs b/Source/Client/MultiplayerSession.cs index 7219d0de..32d50c1c 100644 --- a/Source/Client/MultiplayerSession.cs +++ b/Source/Client/MultiplayerSession.cs @@ -28,12 +28,13 @@ public class MultiplayerSession : IConnectionStatusListener public ConnectionBase client; public NetManager netClient; - public PacketLogWindow writerLog = new(); - public PacketLogWindow readerLog = new(); + public PacketLogWindow writerLog = new(true); + public PacketLogWindow readerLog = new(false); public int myFactionId; public List players = new(); public GameDataSnapshot dataSnapshot; - public CursorAndPing cursorAndPing = new(); + public LocationPings locationPings = new(); + public PlayerCursors playerCursors = new(); public int autosaveCounter; public float? lastSaveAt; public string desyncTracesFromHost; @@ -42,7 +43,7 @@ public class MultiplayerSession : IConnectionStatusListener public bool replay; public int replayTimerStart = -1; public int replayTimerEnd = -1; - public List events = new(); + public bool showTimeline; public bool desynced; @@ -233,14 +234,14 @@ public void ScheduleCommand(ScheduledCommand cmd) public void Update() { - cursorAndPing.UpdatePing(); + locationPings.UpdatePing(); } public static void DoAutosave() { LongEventHandler.QueueLongEvent(() => { - SaveGameToFile(GetNextAutosaveFileName(), false); + SaveGameToFile_Overwrite(GetNextAutosaveFileName(), false); Multiplayer.Client.Send(Packets.Client_Autosaving); }, "MpSaving", false, null); } @@ -259,7 +260,7 @@ private static string GetNextAutosaveFileName() .First(); } - public static void SaveGameToFile(string fileNameNoExtension, bool currentReplay) + public static void SaveGameToFile_Overwrite(string fileNameNoExtension, bool currentReplay) { Log.Message($"Multiplayer: saving to file {fileNameNoExtension}"); @@ -274,7 +275,7 @@ public static void SaveGameToFile(string fileNameNoExtension, bool currentReplay } catch (Exception e) { - Log.Error($"Exception saving multiplayer game: {e}"); + Log.Error($"Exception saving multiplayer game as {fileNameNoExtension}: {e}"); Messages.Message("MpGameSaveFailed".Translate(), MessageTypeDefOf.SilentInput, false); } } diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 539b7681..57bf6db2 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using System.Text.RegularExpressions; using HarmonyLib; using LiteNetLib; @@ -34,9 +33,20 @@ public static class MultiplayerStatic public static readonly Texture2D DiscordIcon = ContentFinder.Get("Multiplayer/Discord"); public static readonly Texture2D Pulse = ContentFinder.Get("Multiplayer/Pulse"); + public static readonly Texture2D ChangeRelationIcon = ContentFinder.Get("UI/Memes/Loyalist"); + static MultiplayerStatic() { - Native.InitLmfPtr(); + Native.InitLmfPtr( + Application.platform switch + { + RuntimePlatform.LinuxEditor => Native.NativeOS.Linux, + RuntimePlatform.LinuxPlayer => Native.NativeOS.Linux, + RuntimePlatform.OSXEditor => Native.NativeOS.OSX, + RuntimePlatform.OSXPlayer => Native.NativeOS.OSX, + _ => Native.NativeOS.Windows + } + ); // UnityEngine.Debug.Log instead of Verse.Log.Message because the server runs on its own thread ServerLog.info = str => Debug.Log($"MpServerLog: {str}"); @@ -188,7 +198,7 @@ void LoadNextReplay() int ticksDone = 0; double timeSpent = 0; - Replay.LoadReplay(Replay.ReplayFile(current[1]), true, () => + Replay.LoadReplay(Replay.SavedReplayFile(current[1]), true, () => { TickPatch.AllTickables.Do(t => t.SetDesiredTimeSpeed(TimeSpeed.Normal)); @@ -285,7 +295,7 @@ void LogError(string str) // General designation handling { - var designatorFinalizer = AccessTools.Method(typeof(DesignatorPatches), "DesignateFinalizer"); + var designatorFinalizer = AccessTools.Method(typeof(DesignatorPatches), nameof(DesignatorPatches.DesignateFinalizer)); var designatorMethods = new[] { ("DesignateSingleCell", new[]{ typeof(IntVec3) }), ("DesignateMultiCell", new[]{ typeof(IEnumerable) }), @@ -324,7 +334,7 @@ void LogError(string str) var effecterTick = typeof(Effecter).GetMethod(nameof(Effecter.EffectTick)); var effecterTrigger = typeof(Effecter).GetMethod(nameof(Effecter.Trigger)); var effecterCleanup = typeof(Effecter).GetMethod(nameof(Effecter.Cleanup)); - var randomBoltMesh = typeof(LightningBoltMeshPool).GetProperty(nameof(LightningBoltMeshPool.RandomBoltMesh)).GetGetMethod(); + var randomBoltMesh = typeof(LightningBoltMeshPool).GetProperty(nameof(LightningBoltMeshPool.RandomBoltMesh))!.GetGetMethod(); var drawTrackerCtor = typeof(Pawn_DrawTracker).GetConstructor(new[] { typeof(Pawn) }); var randomHair = typeof(PawnStyleItemChooser).GetMethod(nameof(PawnStyleItemChooser.RandomHairFor)); var cannotAssignReason = typeof(Dialog_BeginRitual).GetMethod(nameof(Dialog_BeginRitual.CannotAssignReason), BindingFlags.NonPublic | BindingFlags.Instance); @@ -357,6 +367,8 @@ void LogError(string str) { var thingMethodPrefix = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod("Prefix")); var thingMethodPostfix = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod("Postfix")); + var thingMethodPrefixSpawnSetup = new HarmonyMethod(typeof(ThingMethodPatches).GetMethod(nameof(ThingMethodPatches.Prefix_SpawnSetup))); + var thingMethods = new[] { ("Tick", Type.EmptyTypes), @@ -369,6 +381,11 @@ void LogError(string str) foreach (Type t in typeof(Thing).AllSubtypesAndSelf()) { + // SpawnSetup is patched separately because it sets the map + var spawnSetupMethod = t.GetMethod("SpawnSetup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (spawnSetupMethod != null) + harmony.PatchMeasure(spawnSetupMethod, thingMethodPrefixSpawnSetup, thingMethodPostfix); + foreach ((string m, Type[] args) in thingMethods) { MethodInfo method = t.GetMethod(m, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly, null, args, null); @@ -391,8 +408,8 @@ void LogError(string str) var floatSavePrefix = new HarmonyMethod(typeof(ValueSavePatch).GetMethod(nameof(ValueSavePatch.FloatSave_Prefix))); var valueSaveMethod = typeof(Scribe_Values).GetMethod(nameof(Scribe_Values.Look)); - harmony.PatchMeasure(valueSaveMethod.MakeGenericMethod(typeof(double)), doubleSavePrefix, null); - harmony.PatchMeasure(valueSaveMethod.MakeGenericMethod(typeof(float)), floatSavePrefix, null); + harmony.PatchMeasure(valueSaveMethod.MakeGenericMethod(typeof(double)), doubleSavePrefix); + harmony.PatchMeasure(valueSaveMethod.MakeGenericMethod(typeof(float)), floatSavePrefix); } SetCategory("Map time gui patches"); diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 3ed92a3e..2d4b8be8 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -1,24 +1,19 @@ -using HarmonyLib; using Multiplayer.Client.Networking; using Multiplayer.Common; using RimWorld; using System; using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; using Multiplayer.Client.Util; -using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { - [HotSwappable] public static class HostUtil { // Host entry points: @@ -26,22 +21,23 @@ public static class HostUtil // - singleplayer save, ingame // - replay, server browser // - replay, ingame - public static async ClientTask HostServer(ServerSettings settings, bool fromReplay, bool hadSimulation, bool asyncTime) + public static async ClientTask HostServer(ServerSettings settings, bool fromReplay) { Log.Message($"Starting the server"); CreateSession(settings); - // Server already pre-inited in HostWindow - PrepareLocalServer(settings, fromReplay); - if (!fromReplay) SetupGameFromSingleplayer(); + // Server already pre-inited in HostWindow + PrepareLocalServer(settings, fromReplay); + CreateLocalClient(); PrepareGame(); + SetGameState(settings); - Multiplayer.session.dataSnapshot = await CreateGameData(settings, asyncTime); + Multiplayer.session.dataSnapshot = await CreateGameData(); MakeHostOnServer(); @@ -70,13 +66,17 @@ private static void PrepareLocalServer(ServerSettings settings, bool fromReplay) MultiplayerServer.instance = Multiplayer.LocalServer; localServer.hostUsername = Multiplayer.username; - localServer.worldData.defaultFactionId = Faction.OfPlayer.loadID; + localServer.worldData.hostFactionId = Faction.OfPlayer.loadID; + localServer.worldData.spectatorFactionId = Multiplayer.WorldComp.spectatorFaction.loadID; if (settings.steam) localServer.TickEvent += SteamIntegration.ServerSteamNetTick; if (fromReplay) + { localServer.gameTimer = TickPatch.Timer; + localServer.startingTimer = TickPatch.Timer; + } localServer.initDataSource = new TaskCompletionSource(); localServer.CompleteInitData( @@ -98,7 +98,7 @@ private static void PrepareGame() Multiplayer.session.AddMsg(new ChatMsg_Url("https://discord.gg/S4bxXpv"), false); } - private static async Task CreateGameData(ServerSettings settings, bool asyncTime) + private static void SetGameState(ServerSettings settings) { Multiplayer.AsyncWorldTime.SetDesiredTimeSpeed(TimeSpeed.Paused); foreach (var map in Find.Maps) @@ -106,14 +106,17 @@ private static async Task CreateGameData(ServerSettings settin Find.TickManager.CurTimeSpeed = TimeSpeed.Paused; - Multiplayer.GameComp.asyncTime = asyncTime; + Multiplayer.GameComp.asyncTime = settings.asyncTime; + Multiplayer.GameComp.multifaction = settings.multifaction; Multiplayer.GameComp.debugMode = settings.debugMode; Multiplayer.GameComp.logDesyncTraces = settings.desyncTraces; Multiplayer.GameComp.pauseOnLetter = settings.pauseOnLetter; Multiplayer.GameComp.timeControl = settings.timeControl; + } + private static async Task CreateGameData() + { await LongEventTask.ContinueInLongEvent("MpSaving", false); - return SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); } @@ -121,66 +124,38 @@ private static void SetupGameFromSingleplayer() { var worldComp = new MultiplayerWorldComp(Find.World); - Faction NewFaction(int id, string name, FactionDef def) - { - Faction faction = Find.FactionManager.AllFactions.FirstOrDefault(f => f.loadID == id); - - if (faction == null) - { - faction = new Faction() { loadID = id, def = def }; - - faction.ideos = new FactionIdeosTracker(faction); - faction.ideos.ChooseOrGenerateIdeo(new IdeoGenerationParms()); - - foreach (Faction other in Find.FactionManager.AllFactionsListForReading) - faction.TryMakeInitialRelationsWith(other); - - Find.FactionManager.Add(faction); - - worldComp.factionData[faction.loadID] = FactionWorldData.New(faction.loadID); - } - - faction.Name = name; - faction.def = def; - - return faction; - } - Faction.OfPlayer.Name = $"{Multiplayer.username}'s faction"; //comp.factionData[Faction.OfPlayer.loadID] = FactionWorldData.FromCurrent(); Multiplayer.game = new MultiplayerGame { - gameComp = new MultiplayerGameComp(Current.Game) - { - globalIdBlock = new IdBlock(GetMaxUniqueId(), 1_000_000_000) - }, + gameComp = new MultiplayerGameComp(), asyncWorldTimeComp = new AsyncWorldTimeComp(Find.World) { worldTicks = Find.TickManager.TicksGame }, worldComp = worldComp }; - // var opponent = NewFaction(Multiplayer.GlobalIdBlock.NextId(), "Opponent", FactionDefOf.PlayerColony); - // opponent.hidden = true; - // opponent.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Neutral)); - - foreach (FactionWorldData data in worldComp.factionData.Values) - { - foreach (DrugPolicy p in data.drugPolicyDatabase.policies) - p.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - - foreach (Outfit o in data.outfitDatabase.outfits) - o.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - - foreach (FoodRestriction o in data.foodRestrictionDatabase.foodRestrictions) - o.id = Multiplayer.GlobalIdBlock.NextId(); - } + var spectator = AddNewFaction("Spectator", FactionDefOf.PlayerColony); + spectator.hidden = true; + spectator.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Neutral)); + + worldComp.spectatorFaction = spectator; + + // todo is this needed? + // foreach (FactionWorldData data in worldComp.factionData.Values) + // { + // foreach (DrugPolicy p in data.drugPolicyDatabase.policies) + // p.uniqueId = Multiplayer.GlobalIdBlock.NextId(); + // + // foreach (Outfit o in data.outfitDatabase.outfits) + // o.uniqueId = Multiplayer.GlobalIdBlock.NextId(); + // + // foreach (FoodRestriction o in data.foodRestrictionDatabase.foodRestrictions) + // o.id = Multiplayer.GlobalIdBlock.NextId(); + // } foreach (Map map in Find.Maps) { - //mapComp.mapIdBlock = localServer.NextIdBlock(); - - BeforeMapGeneration.SetupMap(map); - // BeforeMapGeneration.InitNewMapFactionData(map, opponent); + MapSetup.SetupMap(map); AsyncTimeComp async = map.AsyncTime(); async.mapTicks = Find.TickManager.TicksGame; @@ -188,6 +163,25 @@ Faction NewFaction(int id, string name, FactionDef def) } } + public static Faction AddNewFaction(string name, FactionDef def) + { + Faction faction = new Faction { loadID = Find.UniqueIDsManager.GetNextFactionID(), def = def }; + + faction.ideos = new FactionIdeosTracker(faction); + faction.ideos.ChooseOrGenerateIdeo(new IdeoGenerationParms()); + + foreach (Faction other in Find.FactionManager.AllFactionsListForReading) + faction.TryMakeInitialRelationsWith(other); + + faction.Name = name; + faction.def = def; + + Find.FactionManager.Add(faction); + Multiplayer.WorldComp.factionData[faction.loadID] = FactionWorldData.New(faction.loadID); + + return faction; + } + private static void CreateLocalClient() { if (Multiplayer.session.localServerSettings.arbiter) @@ -265,22 +259,5 @@ private static void StartArbiter() } } } - - public static int GetMaxUniqueId() - { - return typeof(UniqueIDsManager) - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Where(f => f.FieldType == typeof(int)) - .Select(f => (int)f.GetValue(Find.UniqueIDsManager)) - .Max(); - } - - public static void SetAllUniqueIds(int value) - { - typeof(UniqueIDsManager) - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Where(f => f.FieldType == typeof(int)) - .Do(f => f.SetValue(Find.UniqueIDsManager, value)); - } } } diff --git a/Source/Client/Networking/JoinData.cs b/Source/Client/Networking/JoinData.cs index 09a203a1..226c2c5f 100644 --- a/Source/Client/Networking/JoinData.cs +++ b/Source/Client/Networking/JoinData.cs @@ -48,7 +48,9 @@ public static byte[] WriteServerData(bool writeConfigs) data.WriteBool(writeConfigs); if (writeConfigs) { - var configs = GetSyncableConfigContents(activeModsSnapshot.Select(m => m.PackageIdNonUnique)); + var configs = GetSyncableConfigContents( + activeModsSnapshot.Select(m => m.PackageIdNonUnique).ToList() + ); data.WriteInt32(configs.Count); foreach (var config in configs) @@ -146,7 +148,7 @@ public static ModMetaData GetInstalledMod(string id) public const string HugsLibId = "unlimitedhugs.hugslib"; public const string HugsLibSettingsFile = "ModSettings"; - public static List GetSyncableConfigContents(IEnumerable modIds) + public static List GetSyncableConfigContents(List modIds) { var list = new List(); @@ -206,7 +208,7 @@ public static bool CompareToLocal(RemoteData remote) remote.remoteRwVersion == VersionControl.CurrentVersionString && remote.CompareMods(activeModsSnapshot) == ModListDiff.None && remote.remoteFiles.DictsEqual(modFilesSnapshot) && - (!remote.hasConfigs || remote.remoteModConfigs.EqualAsSets(GetSyncableConfigContents(remote.RemoteModIds))); + (!remote.hasConfigs || remote.remoteModConfigs.EqualAsSets(GetSyncableConfigContents(remote.RemoteModIds.ToList()))); } internal static void TakeModDataSnapshot() @@ -355,7 +357,7 @@ public ModFileDict CopyWithMods(IEnumerable modIds) public struct ModInfo { - public string packageId; // Mod package id with no _steam suffix + public string packageId; // Mod package id, lower case, no _steam suffix public string name; public ulong steamId; // Zero means invalid public ContentSource source; diff --git a/Source/Client/Networking/State/ClientJoiningState.cs b/Source/Client/Networking/State/ClientJoiningState.cs index c9d973a5..c1b08aed 100644 --- a/Source/Client/Networking/State/ClientJoiningState.cs +++ b/Source/Client/Networking/State/ClientJoiningState.cs @@ -2,13 +2,11 @@ using Multiplayer.Client.Networking; using Multiplayer.Common; using System.Linq; -using Multiplayer.Common.Util; using RimWorld; using Verse; namespace Multiplayer.Client { - [HotSwappable] public class ClientJoiningState : ClientBaseState { public ClientJoiningState(ConnectionBase connection) : base(connection) diff --git a/Source/Client/Networking/State/ClientLoadingState.cs b/Source/Client/Networking/State/ClientLoadingState.cs index 2b41812a..503083f5 100644 --- a/Source/Client/Networking/State/ClientLoadingState.cs +++ b/Source/Client/Networking/State/ClientLoadingState.cs @@ -84,6 +84,7 @@ public void HandleWorldData(ByteReader data) TickPatch.tickUntil = tickUntil; Multiplayer.session.receivedCmds = remoteSentCmds; + Multiplayer.session.remoteTickUntil = tickUntil; TickPatch.serverFrozen = serverFrozen; int syncInfos = data.ReadInt32(); diff --git a/Source/Client/Networking/State/ClientPlayingState.cs b/Source/Client/Networking/State/ClientPlayingState.cs index 4ab3aabf..e10f6e27 100644 --- a/Source/Client/Networking/State/ClientPlayingState.cs +++ b/Source/Client/Networking/State/ClientPlayingState.cs @@ -176,7 +176,7 @@ public void HandlePing(ByteReader data) int planetTile = data.ReadInt32(); var loc = new Vector3(data.ReadFloat(), data.ReadFloat(), data.ReadFloat()); - Session.cursorAndPing.ReceivePing(player, map, planetTile, loc); + Session.locationPings.ReceivePing(player, map, planetTile, loc); } [PacketHandler(Packets.Server_MapResponse)] @@ -251,6 +251,7 @@ public void HandleTraces(ByteReader data) [PacketHandler(Packets.Server_Debug)] public void HandleDebug(ByteReader data) { + MultiplayerSession.DoRejoin(); } [PacketHandler(Packets.Server_SetFaction)] diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 0ce4cebd..b07999b6 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -43,7 +43,7 @@ public void Update() SyncFieldUtil.UpdateSync(); if (!Multiplayer.arbiterInstance && Application.isFocused && !TickPatch.Simulating && !Multiplayer.session.desynced) - Multiplayer.session.cursorAndPing.SendVisuals(); + Multiplayer.session.playerCursors.SendVisuals(); if (Multiplayer.Client is SteamBaseConn steamConn && SteamManager.Initialized) foreach (var packet in SteamIntegration.ReadPackets(steamConn.recvChannel)) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 3abdfac5..d4aab3bf 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -297,7 +297,7 @@ static IEnumerable Transpiler(IEnumerable inst var anythingPatched = false; var parameters = new[] { typeof(int), typeof(int) }; - var unityRandomRangeInt = AccessTools.DeclaredMethod(typeof(UnityEngine.Random), nameof(UnityEngine.Random.Range), parameters); + var unityRandomRangeInt = AccessTools.DeclaredMethod(typeof(Random), nameof(Random.Range), parameters); var verseRandomRangeInt = AccessTools.DeclaredMethod(typeof(Rand), nameof(Rand.Range), parameters); foreach (var inst in insts) @@ -317,4 +317,26 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(Pawn_RecordsTracker), nameof(Pawn_RecordsTracker.ExposeData))] + static class RecordsTrackerExposePatch + { + static IEnumerable Transpiler(IEnumerable insts) + { + var battleActiveField = + AccessTools.Field(typeof(Pawn_RecordsTracker), nameof(Pawn_RecordsTracker.battleActive)); + + foreach (var inst in insts) + { + // Remove mutation of battleActive during saving which was a source of non-determinism + if (inst.opcode == OpCodes.Stfld && inst.operand == battleActiveField) + { + yield return new CodeInstruction(OpCodes.Pop); + yield return new CodeInstruction(OpCodes.Pop); + } + else + yield return inst; + } + } + } + } diff --git a/Source/Client/Patches/EarlyPatchAttribute.cs b/Source/Client/Patches/EarlyPatchAttribute.cs index 3cd2de36..19a55923 100644 --- a/Source/Client/Patches/EarlyPatchAttribute.cs +++ b/Source/Client/Patches/EarlyPatchAttribute.cs @@ -3,7 +3,7 @@ namespace Multiplayer.Client.Patches { /// - /// Indicates that the patch should run right after Mod instance construction + /// Indicates that the patch should run during Mod instance construction /// (not in the static constructor of MultiplayerStatic) /// public class EarlyPatchAttribute : Attribute diff --git a/Source/Client/Patches/Letters.cs b/Source/Client/Patches/Letters.cs index f7e1ea86..93ba7396 100644 --- a/Source/Client/Patches/Letters.cs +++ b/Source/Client/Patches/Letters.cs @@ -117,7 +117,7 @@ static bool Prefix(Letter let) => [HarmonyPatch(typeof(Dialog_GrowthMomentChoices), nameof(Dialog_GrowthMomentChoices.DoWindowContents))] static class IsDrawingGrowthMomentDialog { - public static bool isDrawing = false; + public static bool isDrawing; static void Prefix() => isDrawing = true; diff --git a/Source/Client/Patches/MapSetup.cs b/Source/Client/Patches/MapSetup.cs new file mode 100644 index 00000000..e9be79eb --- /dev/null +++ b/Source/Client/Patches/MapSetup.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using HarmonyLib; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +[HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] +public static class MapSetup +{ + static void Prefix(ref Action extraInitBeforeContentGen) + { + if (Multiplayer.Client == null) return; + extraInitBeforeContentGen += SetupMap; + } + + static void Postfix() + { + if (Multiplayer.Client == null) return; + + Log.Message("Rand " + Rand.StateCompressed); + } + + public static void SetupMap(Map map) + { + Log.Message("New map " + map.uniqueID); + Log.Message("Rand " + Rand.StateCompressed); + + // Initialize and store Multiplayer components + var async = new AsyncTimeComp(map); + Multiplayer.game.asyncTimeComps.Add(async); + + var mapComp = new MultiplayerMapComp(map); + Multiplayer.game.mapComps.Add(mapComp); + + // Store all current managers for Faction.OfPlayer + InitFactionDataFromMap(map, Faction.OfPlayer); + + // Add all other (non Faction.OfPlayer) factions to the map + foreach (var faction in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) + if (faction != Faction.OfPlayer) + InitNewFactionData(map, faction); + + async.mapTicks = Find.Maps.Where(m => m != map).Select(m => m.AsyncTime()?.mapTicks).Max() ?? Find.TickManager.TicksGame; + async.storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty); + async.storyWatcher = new StoryWatcher(); + + if (!Multiplayer.GameComp.asyncTime) + async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); + } + + private static void InitFactionDataFromMap(Map map, Faction f) + { + var mapComp = map.MpComp(); + mapComp.factionData[f.loadID] = FactionMapData.NewFromMap(map, f.loadID); + + var customData = mapComp.customFactionData[f.loadID] = CustomFactionMapData.New(f.loadID, map); + + foreach (var t in map.listerThings.AllThings) + if (t is ThingWithComps tc && + tc.GetComp() is { forbiddenInt: false }) + customData.unforbidden.Add(t); + } + + public static void InitNewFactionData(Map map, Faction f) + { + var mapComp = map.MpComp(); + + mapComp.factionData[f.loadID] = FactionMapData.New(f.loadID, map); + mapComp.factionData[f.loadID].areaManager.AddStartingAreas(); + + mapComp.customFactionData[f.loadID] = CustomFactionMapData.New(f.loadID, map); + } +} diff --git a/Source/Client/Patches/Optimizations.cs b/Source/Client/Patches/Optimizations.cs index ef727710..6a3299fe 100644 --- a/Source/Client/Patches/Optimizations.cs +++ b/Source/Client/Patches/Optimizations.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using Verse; @@ -5,7 +6,7 @@ namespace Multiplayer.Client { static class ThingCategoryDef_DescendantThingDefsPatch { - static Dictionary> values = new(DefaultComparer.Instance); + static ConcurrentDictionary> values = new(DefaultComparer.Instance); static bool Prefix(ThingCategoryDef __instance) { @@ -28,7 +29,7 @@ static void Postfix(ThingCategoryDef __instance, ref IEnumerable __res static class ThingCategoryDef_ThisAndChildCategoryDefsPatch { - static Dictionary> values = new(DefaultComparer.Instance); + static ConcurrentDictionary> values = new(DefaultComparer.Instance); static bool Prefix(ThingCategoryDef __instance) { diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 53127bf5..8a1ac633 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -9,7 +9,6 @@ using System.Reflection.Emit; using System.Text.RegularExpressions; using System.Xml.Linq; -using Multiplayer.Common.Util; using UnityEngine; using Verse; using Verse.AI; @@ -72,41 +71,6 @@ static void Postfix(ref bool __result) } } - [HarmonyPatch(typeof(AutoBuildRoofAreaSetter))] - [HarmonyPatch(nameof(AutoBuildRoofAreaSetter.TryGenerateAreaNow))] - public static class AutoRoofPatch - { - static bool Prefix(AutoBuildRoofAreaSetter __instance, Room room, ref Map __state) - { - if (Multiplayer.Client == null) return true; - if (room.Dereferenced || room.TouchesMapEdge || room.RegionCount > 26 || room.CellCount > 320 || room.IsDoorway) return false; - - Map map = room.Map; - Faction faction = null; - - foreach (IntVec3 cell in room.BorderCells) - { - Thing holder = cell.GetRoofHolderOrImpassable(map); - if (holder == null || holder.Faction == null) continue; - if (faction != null && holder.Faction != faction) return false; - faction = holder.Faction; - } - - if (faction == null) return false; - - map.PushFaction(faction); - __state = map; - - return true; - } - - static void Postfix(ref Map __state) - { - if (__state != null) - __state.PopFaction(); - } - } - /* [HarmonyPatch(typeof(Thing), nameof(Thing.ExposeData))] public static class PawnExposeDataFirst @@ -244,23 +208,23 @@ static void Postfix(KeyBindingDef __instance, ref bool __result) [HarmonyPatch(typeof(Pawn), nameof(Pawn.SpawnSetup))] static class PawnSpawnSetupMarker { - public static bool respawningAfterLoad; + public static bool currentlyRespawningAfterLoad; static void Prefix(bool respawningAfterLoad) { - PawnSpawnSetupMarker.respawningAfterLoad = respawningAfterLoad; + currentlyRespawningAfterLoad = respawningAfterLoad; } static void Finalizer() { - respawningAfterLoad = false; + currentlyRespawningAfterLoad = false; } } [HarmonyPatch(typeof(Pawn_PathFollower), nameof(Pawn_PathFollower.ResetToCurrentPosition))] static class PatherResetPatch { - static bool Prefix() => !PawnSpawnSetupMarker.respawningAfterLoad; + static bool Prefix() => !PawnSpawnSetupMarker.currentlyRespawningAfterLoad; } [HarmonyPatch(typeof(Game), nameof(Game.LoadGame))] @@ -311,69 +275,6 @@ static class DontEnlistNonSaveableThings static bool Prefix(Thing t) => t.def.isSaveable; } - [HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] - static class BeforeMapGeneration - { - static void Prefix(ref Action extraInitBeforeContentGen) - { - if (Multiplayer.Client == null) return; - extraInitBeforeContentGen += SetupMap; - } - - static void Postfix() - { - if (Multiplayer.Client == null) return; - - Log.Message("Unique ids " + Multiplayer.GlobalIdBlock.currentWithinBlock); - Log.Message("Rand " + Rand.StateCompressed); - } - - public static void SetupMap(Map map) - { - Log.Message("New map " + map.uniqueID); - Log.Message("Unique ids " + Multiplayer.GlobalIdBlock.currentWithinBlock); - Log.Message("Rand " + Rand.StateCompressed); - - var async = new AsyncTimeComp(map); - Multiplayer.game.asyncTimeComps.Add(async); - - var mapComp = new MultiplayerMapComp(map); - Multiplayer.game.mapComps.Add(mapComp); - - InitFactionDataFromMap(map, Faction.OfPlayer); - - async.mapTicks = Find.Maps.Where(m => m != map).Select(m => m.AsyncTime()?.mapTicks).Max() ?? Find.TickManager.TicksGame; - async.storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty); - async.storyWatcher = new StoryWatcher(); - - if (!Multiplayer.GameComp.asyncTime) - async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); - } - - public static void InitFactionDataFromMap(Map map, Faction f) - { - var mapComp = map.MpComp(); - mapComp.factionData[f.loadID] = FactionMapData.NewFromMap(map, f.loadID); - - var customData = mapComp.customFactionData[f.loadID] = CustomFactionMapData.New(f.loadID, map); - - foreach (var t in map.listerThings.AllThings) - if (t is ThingWithComps tc && - tc.GetComp() is { forbiddenInt: false }) - customData.unforbidden.Add(t); - } - - public static void InitNewMapFactionData(Map map, Faction f) - { - var mapComp = map.MpComp(); - - mapComp.factionData[f.loadID] = FactionMapData.New(f.loadID, map); - mapComp.factionData[f.loadID].areaManager.AddStartingAreas(); - - mapComp.customFactionData[f.loadID] = CustomFactionMapData.New(f.loadID, map); - } - } - [HarmonyPatch(typeof(IncidentDef), nameof(IncidentDef.TargetAllowed))] static class GameConditionIncidentTargetPatch { @@ -625,33 +526,24 @@ static void Postfix(bool __state) } } - [HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] - static class SettlementNullFactionPatch1 + [HarmonyPatch(typeof(Game), nameof(Game.LoadGame))] + static class AllowCurrentMapNullWhenLoading { - static bool Prefix(Settlement __instance, ref Material __result) + static IEnumerable Transpiler(IEnumerable insts) { - if (__instance.factionInt == null) - { - __result = BaseContent.BadMat; - return false; - } + var list = insts.ToList(); - return true; - } - } + var strIndex = list.FirstIndexOf(i => + "Current map is null after loading but there are maps available. Setting current map to [0].".Equals(i.operand) + ); - [HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] - static class SettlementNullFactionPatch2 - { - static bool Prefix(Settlement __instance, ref Texture2D __result) - { - if (__instance.factionInt == null) - { - __result = BaseContent.BadTex; - return false; - } + // Remove Log.Error(str) call and setting value=0 + list.RemoveAt(strIndex); + list.RemoveAt(strIndex); + list.RemoveAt(strIndex); + list.RemoveAt(strIndex); - return true; + return list; } } } diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index 24955862..196c3e90 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using Multiplayer.Client.Patches; using RimWorld; using RimWorld.Planet; using System; @@ -42,12 +41,6 @@ static void Prefix(Map __instance, ref bool __state) int seed = __instance.uniqueID; Rand.PushState(seed); - if (Scribe.mode != LoadSaveMode.LoadingVars) - { - //UniqueIdsPatch.CurrentBlock = __instance.MpComp().mapIdBlock; - UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; - } - __state = true; } @@ -57,9 +50,6 @@ static void Postfix(Map __instance, bool __state) { Log.Message($"Map.ExposeData post rand {__instance.uniqueID} {Scribe.mode} {Rand.iterations}"); Rand.PopState(); - - if (Scribe.mode != LoadSaveMode.LoadingVars) - UniqueIdsPatch.CurrentBlock = null; } } } @@ -75,9 +65,6 @@ static void Prefix(Map __instance, ref bool __state) int seed = __instance.uniqueID; Rand.PushState(seed); - //UniqueIdsPatch.CurrentBlock = __instance.MpComp().mapIdBlock; - UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; - __state = true; } @@ -87,12 +74,11 @@ static void Postfix(Map __instance, bool __state) { Log.Message($"Map.FinalizeLoading post rand {__instance.uniqueID} {Rand.iterations}"); Rand.PopState(); - UniqueIdsPatch.CurrentBlock = null; } } } - [HarmonyPatch(typeof(CaravanEnterMapUtility), nameof(CaravanEnterMapUtility.Enter), new[] { typeof(Caravan), typeof(Map), typeof(CaravanEnterMode), typeof(CaravanDropInventoryMode), typeof(bool), typeof(Predicate) })] + [HarmonyPatch(typeof(CaravanEnterMapUtility), nameof(CaravanEnterMapUtility.Enter), new[] { typeof(Caravan), typeof(Map), typeof(Func), typeof(CaravanDropInventoryMode), typeof(bool) })] static class SeedCaravanEnter { static void Prefix(Map map, ref bool __state) diff --git a/Source/Client/Patches/StylingStation.cs b/Source/Client/Patches/StylingStation.cs index 828f8244..668dfe12 100644 --- a/Source/Client/Patches/StylingStation.cs +++ b/Source/Client/Patches/StylingStation.cs @@ -82,7 +82,7 @@ static class StylingDialog_CloseOnDestroyed static bool Prefix(Dialog_StylingStation __instance) { // The styling station got destroyed (in multiplayer, the styling dialog is not pausing) - if (__instance.stylingStation != null && !__instance.stylingStation.Spawned) + if (__instance.stylingStation is { Spawned: false }) { __instance.Close(); return false; diff --git a/Source/Client/Patches/ThingMethodPatches.cs b/Source/Client/Patches/ThingMethodPatches.cs index cfd104cc..1e161520 100644 --- a/Source/Client/Patches/ThingMethodPatches.cs +++ b/Source/Client/Patches/ThingMethodPatches.cs @@ -1,6 +1,7 @@ using HarmonyLib; using RimWorld; using System.Collections.Generic; +using Multiplayer.Client.Factions; using Verse; using Verse.AI; @@ -19,8 +20,8 @@ static void Prefix(Pawn pawn, ref Container? __state) static void Postfix(Pawn pawn, Container? __state) { - if (__state != null) - __state.PopFaction(); + if (__state is { Inner: var map }) + map.PopFaction(); } } @@ -37,8 +38,8 @@ static void Prefix(Pawn ___pawn, ref Container? __state) static void Postfix(Container? __state) { - if (__state != null) - __state.PopFaction(); + if (__state is { Inner: var map }) + map.PopFaction(); } } @@ -65,8 +66,8 @@ static void Prefix(Pawn_JobTracker __instance, Job newJob, ref Container? _ static void Postfix(Container? __state) { - if (__state != null) - __state.PopFaction(); + if (__state is { Inner: var map }) + map.PopFaction(); } } @@ -87,8 +88,8 @@ static void Prefix(Pawn_JobTracker __instance, JobCondition condition, ref Conta static void Postfix(Container? __state) { - if (__state != null) - __state.PopFaction(); + if (__state is { Inner: var map }) + map.PopFaction(); } } @@ -110,9 +111,9 @@ static void Prefix(Pawn_JobTracker __instance, ref Container? __state) static void Postfix(Container? __state) { - if (__state != null) + if (__state is { Inner: var map }) { - __state.PopFaction(); + map.PopFaction(); ThingContext.Pop(); } } @@ -132,13 +133,25 @@ public static void Prefix(Thing __instance, ref Container? __state) __instance.Map.PushFaction(__instance.Faction); } + [HarmonyPriority(MpPriority.MpFirst)] + public static void Prefix_SpawnSetup(Thing __instance, Map map, ref Container? __state) + { + if (Multiplayer.Client == null) return; + + __state = map; + ThingContext.Push(__instance); + + if (__instance.def.CanHaveFaction) + map.PushFaction(__instance.Faction); + } + [HarmonyPriority(MpPriority.MpLast)] public static void Postfix(Thing __instance, Container? __state) { - if (__state == null) return; + if (__state is not { Inner: var map }) return; if (__instance.def.CanHaveFaction) - __state.PopFaction(); + map.PopFaction(); ThingContext.Pop(); } diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index 3b4da9df..aea78d6e 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -6,20 +6,18 @@ using System.Diagnostics; using System.Linq; using Multiplayer.Client.AsyncTime; -using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { [HarmonyPatch(typeof(TickManager), nameof(TickManager.TickManagerUpdate))] - [HotSwappable] public static class TickPatch { public static int Timer { get; private set; } public static int ticksToRun; - public static int tickUntil; + public static int tickUntil; // Ticks < tickUntil can be simulated public static int workTicks; public static bool currentExecutingCmdIssuedBySelf; public static bool serverFrozen; @@ -129,7 +127,7 @@ private static void CheckFinishSimulating() } } - public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action onFinish = null, Action onCancel = null, string cancelButtonKey = null, bool canESC = false, string simTextKey = null) + public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action onFinish = null, Action onCancel = null, string cancelButtonKey = null, bool canEsc = false, string simTextKey = null) { simulating = new SimulatingData { @@ -137,7 +135,7 @@ public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action targetIsTickUntil = toTickUntil, onFinish = onFinish, onCancel = onCancel, - canEsc = canESC, + canEsc = canEsc, cancelButtonKey = cancelButtonKey ?? "CancelButton", simTextKey = simTextKey ?? "MpSimulating" }; @@ -182,7 +180,7 @@ public static void DoTicks(int ticks) } // Returns whether the tick loop should stop - private static bool DoTick(ref bool worked) + public static bool DoTick(ref bool worked, bool justCmds = false) { tickTimer.Restart(); int curTimer = Timer; @@ -198,6 +196,9 @@ private static bool DoTick(ref bool worked) } } + if (justCmds) + return true; + foreach (ITickable tickable in AllTickables) { if (tickable.TimePerTick(tickable.DesiredTimeSpeed) == 0) continue; diff --git a/Source/Client/Patches/TileTemperatures.cs b/Source/Client/Patches/TileTemperatures.cs index ca7bcfaf..328f0882 100644 --- a/Source/Client/Patches/TileTemperatures.cs +++ b/Source/Client/Patches/TileTemperatures.cs @@ -50,7 +50,6 @@ static void Prefix(TileTemperaturesComp __instance) } } - [HarmonyPatch(typeof(GenTemperature), nameof(GenTemperature.AverageTemperatureAtTileForTwelfth))] static class CacheAverageTileTemperature { diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index 69faeb5a..d8055a3e 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -1,70 +1,32 @@ using HarmonyLib; -using Multiplayer.Common; using RimWorld; using System.Collections.Generic; using System.Reflection; -using Multiplayer.Common.Util; +using Multiplayer.Client.Desyncs; using Verse; namespace Multiplayer.Client.Patches { [HarmonyPatch(typeof(UniqueIDsManager))] [HarmonyPatch(nameof(UniqueIDsManager.GetNextID))] - [HotSwappable] public static class UniqueIdsPatch { - private static IdBlock currentBlock; - - public static IdBlock CurrentBlock - { - get => currentBlock; - - set - { - if (value != null && currentBlock != null && currentBlock != value) - Log.Warning("Reassigning the current id block!"); - currentBlock = value; - } - } - // Start at -2 because -1 is sometimes used as the uninitialized marker private static int localIds = -2; static bool Prefix() { - return Multiplayer.Client == null || !Multiplayer.InInterface; + return Multiplayer.Client == null || (!Multiplayer.InInterface && Current.ProgramState != ProgramState.Entry); } static void Postfix(ref int __result) { if (Multiplayer.Client == null) return; - // if (currentBlockBlock == null) - // { - // __result = localIds--; - // if (!Multiplayer.ShouldSync) - // Log.Warning("Tried to get a unique id without an id block set!"); - // return; - // } - // - // __result = CurrentBlock.NextId(); - - if (Multiplayer.InInterface) - { + if (Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry) __result = localIds--; - } else - { - __result = Multiplayer.GlobalIdBlock.NextId(); - } - - //MpLog.Log("got new id " + __result); - - /*if (currentBlock.current > currentBlock.blockSize * 0.95f && !currentBlock.overflowHandled) - { - Multiplayer.Client.Send(Packets.Client_IdBlockRequest, CurrentBlock.mapId); - currentBlock.overflowHandled = true; - }*/ + DeferredStackTracing.Postfix(); } } @@ -81,36 +43,6 @@ static IEnumerable TargetMethods() static bool Prefix() => Scribe.mode != LoadSaveMode.LoadingVars; } - [HarmonyPatch(typeof(OutfitDatabase), nameof(OutfitDatabase.MakeNewOutfit))] - static class OutfitUniqueIdPatch - { - static void Postfix(Outfit __result) - { - if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) - __result.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - } - } - - [HarmonyPatch(typeof(DrugPolicyDatabase), nameof(DrugPolicyDatabase.MakeNewDrugPolicy))] - static class DrugPolicyUniqueIdPatch - { - static void Postfix(DrugPolicy __result) - { - if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) - __result.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - } - } - - [HarmonyPatch(typeof(FoodRestrictionDatabase), nameof(FoodRestrictionDatabase.MakeNewFoodRestriction))] - static class FoodRestrictionUniqueIdPatch - { - static void Postfix(FoodRestriction __result) - { - if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) - __result.id = Multiplayer.GlobalIdBlock.NextId(); - } - } - [HarmonyPatch] static class MessagesMarker { @@ -158,4 +90,43 @@ static bool Prefix(IArchivable archivable) } } + [HarmonyPatch(typeof(CrossRefHandler), nameof(CrossRefHandler.Clear))] + static class CrossRefHandler_Clear_Patch + { + static void Prefix(CrossRefHandler __instance, bool errorIfNotEmpty) + { + // If called from CrossRefHandler.ResolveAllCrossReferences and exposing during playtime, fix object ids + if (errorIfNotEmpty && ScribeUtil.loading) + { + foreach (var key in ScribeUtil.sharedCrossRefs.tempKeys) + { + var obj = ScribeUtil.sharedCrossRefs.allObjectsByLoadID[key]; + if (obj is Thing { thingIDNumber: < 0 } t) + t.thingIDNumber = Find.UniqueIDsManager.GetNextThingID(); + else if (obj is Gene { loadID: < 0 } g) + g.loadID = Find.UniqueIDsManager.GetNextGeneID(); + else if (obj is Hediff { loadID: < 0 } h) + h.loadID = Find.UniqueIDsManager.GetNextHediffID(); + } + } + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.IDNumberFromThingID))] + static class HandleNegativeThingIdWhenLoading + { + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + // Handle negative thing ids when loading + // These come from getting a unique id in the interface and are fixed (replaced) later when necessary + if (inst.operand == "\\d+$") + inst.operand = "-?\\d+$"; + + yield return inst; + } + } + } + } diff --git a/Source/Client/Patches/VanillaTweaks.cs b/Source/Client/Patches/VanillaTweaks.cs index 43422113..b94818b6 100644 --- a/Source/Client/Patches/VanillaTweaks.cs +++ b/Source/Client/Patches/VanillaTweaks.cs @@ -19,7 +19,7 @@ static void Prefix() { if (done) return; GUI.skin.font = Text.fontStyles[1].font; - Text.fontStyles[1].font.fontNames = new string[] { "arial", "arialbd", "ariali", "arialbi" }; + Text.fontStyles[1].font.fontNames = new[] { "arial", "arialbd", "ariali", "arialbi" }; done = true; } } @@ -58,6 +58,7 @@ static class LongEventWindowPreventCameraMotion { public const int LongEventWindowId = 62893994; + // ReSharper disable once InconsistentNaming static void Postfix(int ID) { if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) diff --git a/Source/Client/Patches/WorldPawns.cs b/Source/Client/Patches/WorldPawns.cs index 2517b1b6..78d1e913 100644 --- a/Source/Client/Patches/WorldPawns.cs +++ b/Source/Client/Patches/WorldPawns.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using HarmonyLib; using Multiplayer.API; using RimWorld.Planet; using Verse; diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 8b381723..285be38b 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -4,12 +4,10 @@ using RimWorld.Planet; using System; using System.Collections.Generic; -using Multiplayer.Common.Util; using Verse; namespace Multiplayer.Client { - [HotSwappable] public class CaravanFormingSession : IExposable, ISessionWithTransferables, IPausingWithDialog { public Map map; @@ -36,8 +34,7 @@ public CaravanFormingSession(Map map) public CaravanFormingSession(Map map, bool reform, Action onClosed, bool mapAboutToBeRemoved, IntVec3? meetingSpot = null) : this(map) { - //sessionId = map.MpComp().mapIdBlock.NextId(); - sessionId = Multiplayer.GlobalIdBlock.NextId(); + sessionId = Find.UniqueIDsManager.GetNextThingID(); this.reform = reform; this.onClosed = onClosed; @@ -60,6 +57,8 @@ private void AddItems() public void OpenWindow(bool sound = true) { + Log.Message($"session {sessionId}"); + var dialog = PrepareDummyDialog(); if (!sound) dialog.soundAppear = null; diff --git a/Source/Client/Persistent/CaravanSplittingPatches.cs b/Source/Client/Persistent/CaravanSplittingPatches.cs index 6588dfce..4427ee1e 100644 --- a/Source/Client/Persistent/CaravanSplittingPatches.cs +++ b/Source/Client/Persistent/CaravanSplittingPatches.cs @@ -18,7 +18,7 @@ static bool Prefix(Window window) //If the dialog being added is a native Dialog_SplitCaravan, cancel adding it to the window stack. //Otherwise, window being added is something else. Let it happen. - return !(window is Dialog_SplitCaravan) || window is CaravanSplittingProxy; + return window is CaravanSplittingProxy or not Dialog_SplitCaravan; } } diff --git a/Source/Client/Persistent/CaravanSplittingProxy.cs b/Source/Client/Persistent/CaravanSplittingProxy.cs index 54453433..69108cdd 100644 --- a/Source/Client/Persistent/CaravanSplittingProxy.cs +++ b/Source/Client/Persistent/CaravanSplittingProxy.cs @@ -32,7 +32,7 @@ public override void PostOpen() // Taken from Window.PostOpen, overriden to remove effects of Dialog_SplitCaravan.PostOpen if (soundAppear != null) - soundAppear.PlayOneShotOnCamera(null); + soundAppear.PlayOneShotOnCamera(); if (soundAmbient != null) sustainerAmbient = soundAmbient.TrySpawnSustainer(SoundInfo.OnCamera(MaintenanceType.PerFrame)); diff --git a/Source/Client/Persistent/CaravanSplittingSession.cs b/Source/Client/Persistent/CaravanSplittingSession.cs index a9ae5448..6490e742 100644 --- a/Source/Client/Persistent/CaravanSplittingSession.cs +++ b/Source/Client/Persistent/CaravanSplittingSession.cs @@ -47,7 +47,7 @@ public class CaravanSplittingSession : IExposable, ISessionWithTransferables, IP /// public CaravanSplittingSession(Caravan caravan) { - sessionId = Multiplayer.GlobalIdBlock.NextId(); + sessionId = Find.UniqueIDsManager.GetNextThingID(); Caravan = caravan; AddItems(); @@ -86,8 +86,7 @@ public void OpenWindow(bool sound = true) IgnorePawnsInventoryMode.Ignore, () => dialog.DestMassCapacity - dialog.DestMassUsage, false, - Caravan.Tile, - false + Caravan.Tile ); dialog.CountToTransferChanged(); diff --git a/Source/Client/Persistent/PersistentDialogs.cs b/Source/Client/Persistent/PersistentDialogs.cs index 2f49b249..fcf1e65e 100644 --- a/Source/Client/Persistent/PersistentDialogs.cs +++ b/Source/Client/Persistent/PersistentDialogs.cs @@ -243,7 +243,7 @@ protected PersistentDialog(Map map) : base(map) protected PersistentDialog(Map map, T dialog) : this(map) { - id = Multiplayer.GlobalIdBlock.NextId(); + id = Find.UniqueIDsManager.GetNextThingID(); this.dialog = dialog; } @@ -460,6 +460,9 @@ public FieldSave(PersistentDialog parent) this.parent = parent; } + public const LookMode DelegateMode = (LookMode)101; + public const LookMode PlainObjectMode = (LookMode)100; + public FieldSave(PersistentDialog parent, Type type, object value) : this(parent) { this.type = type; @@ -467,7 +470,7 @@ public FieldSave(PersistentDialog parent, Type type, object value) : this(par this.value = value; if (typeof(Delegate).IsAssignableFrom(type)) - mode = (LookMode)101; + mode = DelegateMode; else if (ParseHelper.HandlesType(type)) mode = LookMode.Value; else if (typeof(Def).IsAssignableFrom(type)) @@ -479,12 +482,12 @@ public FieldSave(PersistentDialog parent, Type type, object value) : this(par else if (typeof(IExposable).IsAssignableFrom(type)) mode = LookMode.Deep; else - mode = (LookMode)100; + mode = PlainObjectMode; } private Dictionary fields; - private string methodType; + private string methodDeclaringType; private string methodName; private int targetIndex; @@ -524,7 +527,7 @@ public void ExposeData() if (Scribe.mode == LoadSaveMode.LoadingVars) value = args[0]; } - else if (mode == (LookMode)100) + else if (mode == PlainObjectMode) { if (Scribe.mode == LoadSaveMode.Saving) { @@ -538,17 +541,20 @@ public void ExposeData() if (Scribe.mode == LoadSaveMode.LoadingVars) { + if (!type.IsCompilerGenerated()) + throw new Exception($"Persistent dialog field deserialization: Unsupported plain object type: {type.FullName}"); + value = Activator.CreateInstance(type); Scribe_Collections.Look(ref fields, "fields"); } - if (Scribe.mode == LoadSaveMode.PostLoadInit) + if (Scribe.mode == LoadSaveMode.PostLoadInit && value != null) { foreach (var kv in fields) value.SetPropertyOrField(kv.Key, parent.fieldValues[kv.Value].value); } } - else if (mode == (LookMode)101) + else if (mode == DelegateMode) { if (Scribe.mode == LoadSaveMode.Saving) { @@ -563,7 +569,7 @@ public void ExposeData() if (Scribe.mode == LoadSaveMode.LoadingVars) { - Scribe_Values.Look(ref methodType, "methodType"); + Scribe_Values.Look(ref methodDeclaringType, "methodType"); Scribe_Values.Look(ref methodName, "methodName"); Scribe_Values.Look(ref targetIndex, "targetIndex"); } @@ -573,11 +579,20 @@ public void ExposeData() if (targetIndex != -1) { object target = parent.fieldValues[targetIndex].value; - value = Delegate.CreateDelegate(type, target, methodName); + value = Delegate.CreateDelegate( + type, + target, + DelegateSerialization.CheckMethodAllowed( + AccessTools.Method(target.GetType(), methodName)) + ); } else { - value = Delegate.CreateDelegate(type, GenTypes.GetTypeInAnyAssembly(methodType), methodName); + value = Delegate.CreateDelegate( + type, + DelegateSerialization.CheckMethodAllowed( + AccessTools.Method(GenTypes.GetTypeInAnyAssembly(methodDeclaringType), methodName)) + ); } } } diff --git a/Source/Client/Persistent/RitualData.cs b/Source/Client/Persistent/RitualData.cs index d1f23d92..a982b387 100644 --- a/Source/Client/Persistent/RitualData.cs +++ b/Source/Client/Persistent/RitualData.cs @@ -1,10 +1,6 @@ -using HarmonyLib; using Multiplayer.API; -using Multiplayer.Common; using RimWorld; -using System; using System.Collections.Generic; -using System.Linq; using Verse; using static RimWorld.Dialog_BeginRitual; @@ -32,9 +28,9 @@ public void Sync(SyncWorker sync) sync.Bind(ref extraInfos); if (sync is WritingSyncWorker writer1) - WriteDelegate(writer1.writer, action); + DelegateSerialization.WriteDelegate(writer1.writer, action); else if (sync is ReadingSyncWorker reader) - action = (ActionCallback)ReadDelegate(reader.reader); + action = (ActionCallback)DelegateSerialization.ReadDelegate(reader.reader); sync.Bind(ref ritualLabel); sync.Bind(ref confirmText); @@ -45,104 +41,5 @@ public void Sync(SyncWorker sync) else if (sync is ReadingSyncWorker reader) reader.Bind(ref assignments, new SyncType(typeof(MpRitualAssignments)) { expose = true }); } - - private static void WriteDelegate(ByteWriter writer, Delegate del) - { - writer.WriteBool(del != null); - if (del == null) return; - - SyncSerialization.WriteSync(writer, del.GetType()); - SyncSerialization.WriteSync(writer, del.Method.DeclaringType); - SyncSerialization.WriteSync(writer, del.Method.Name); // todo Handle the signature for ambiguous methods - - writer.WriteBool(del.Target != null); - if (del.Target != null) - { - var targetType = del.Target.GetType(); - SyncSerialization.WriteSync(writer, targetType); - - var fieldPaths = GetFields(targetType).ToArray(); - var fieldTypes = fieldPaths.Select(MpReflection.PathType).ToArray(); - - void SyncObj(object obj, Type type, string debugInfo) - { - if (type.IsCompilerGenerated()) - return; - - (writer as LoggingByteWriter)?.Log.Enter(debugInfo); - - try - { - if (typeof(Delegate).IsAssignableFrom(type)) - WriteDelegate(writer, (Delegate)obj); - else - SyncSerialization.WriteSyncObject(writer, obj, type); - } - finally - { - (writer as LoggingByteWriter)?.Log.Exit(); - } - } - - for (int i = 0; i < fieldPaths.Length; i++) - SyncObj(del.Target.GetPropertyOrField(fieldPaths[i]), fieldTypes[i], fieldPaths[i]); - } - } - - private static Delegate ReadDelegate(ByteReader reader) - { - if (!reader.ReadBool()) return null; - - var delegateType = SyncSerialization.ReadSync(reader); - var type = SyncSerialization.ReadSync(reader); - var methodName = SyncSerialization.ReadSync(reader); - object target = null; - - if (reader.ReadBool()) - { - var targetType = SyncSerialization.ReadSync(reader); - var fieldPaths = GetFields(targetType).ToArray(); - var fieldTypes = fieldPaths.Select(path => MpReflection.PathType(path)).ToArray(); - - target = Activator.CreateInstance(targetType); - - for (int i = 0; i < fieldPaths.Length; i++) - { - string path = fieldPaths[i]; - string noTypePath = MpReflection.RemoveType(path); - Type fieldType = fieldTypes[i]; - object value; - - if (fieldType.IsCompilerGenerated()) - value = Activator.CreateInstance(fieldType); - else if (typeof(Delegate).IsAssignableFrom(fieldType)) - value = ReadDelegate(reader); - else - value = SyncSerialization.ReadSyncObject(reader, fieldType); - - MpReflection.SetValue(target, path, value); - } - } - - return Delegate.CreateDelegate( - delegateType, - target, - AccessTools.Method(type, methodName) - ); - } - - const string CachedLambda = "<>9"; - - private static List GetFields(Type targetType) - { - var fieldList = new List(); - SyncDelegate.AllDelegateFieldsRecursive( - targetType, - path => { if (!path.Contains(CachedLambda)) fieldList.Add(path); return false; }, - allowDelegates: true - ); - - return fieldList; - } - }; + } } diff --git a/Source/Client/Persistent/Rituals.cs b/Source/Client/Persistent/Rituals.cs index feeab9fd..b1798def 100644 --- a/Source/Client/Persistent/Rituals.cs +++ b/Source/Client/Persistent/Rituals.cs @@ -27,7 +27,7 @@ public RitualSession(Map map) public RitualSession(Map map, RitualData data) { - SessionId = Multiplayer.GlobalIdBlock.NextId(); + SessionId = Find.UniqueIDsManager.GetNextThingID(); this.map = map; this.data = data; diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index b08c2b8e..71716043 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -33,14 +33,14 @@ public string Label } } - public Map Map => null; + public Map Map => playerNegotiator.Map; public int SessionId => sessionId; public MpTradeSession() { } private MpTradeSession(ITrader trader, Pawn playerNegotiator, bool giftMode) { - sessionId = Multiplayer.GlobalIdBlock.NextId(); + sessionId = Find.UniqueIDsManager.GetNextThingID(); this.trader = trader; this.playerNegotiator = playerNegotiator; @@ -61,7 +61,7 @@ public static MpTradeSession TryCreate(ITrader trader, Pawn playerNegotiator, bo Multiplayer.WorldComp.trading.Add(session); CancelTradeDealReset.cancel = true; - SetTradeSession(session, true); + SetTradeSession(session); try { @@ -145,10 +145,8 @@ public void ToggleGiftMode() deal.uiShouldReset = UIShouldReset.Silent; } - public static void SetTradeSession(MpTradeSession session, bool force = false) + public static void SetTradeSession(MpTradeSession session) { - if (!force && TradeSession.deal == session?.deal) return; - current = session; TradeSession.trader = session?.trader; TradeSession.playerNegotiator = session?.playerNegotiator; diff --git a/Source/Client/Persistent/TransporterLoadingSession.cs b/Source/Client/Persistent/TransporterLoadingSession.cs index 8d95ebf2..ec79d759 100644 --- a/Source/Client/Persistent/TransporterLoadingSession.cs +++ b/Source/Client/Persistent/TransporterLoadingSession.cs @@ -28,7 +28,7 @@ public TransporterLoading(Map map) public TransporterLoading(Map map, List transporters) : this(map) { - sessionId = Multiplayer.GlobalIdBlock.NextId(); + sessionId = Find.UniqueIDsManager.GetNextThingID(); this.transporters = transporters; pods = transporters.Select(t => t.parent).ToList(); diff --git a/Source/Client/Saving/ConvertToSp.cs b/Source/Client/Saving/ConvertToSp.cs new file mode 100644 index 00000000..63e8ff6b --- /dev/null +++ b/Source/Client/Saving/ConvertToSp.cs @@ -0,0 +1,47 @@ +using Verse; +using Verse.Profile; + +namespace Multiplayer.Client.Saving; + +public class ConvertToSp +{ + public static void DoConvert() + { + LongEventHandler.QueueLongEvent(() => + { + SaveReplay(); + PrepareSingleplayer(); + PrepareLoading(); + }, "Play", "MpConvertingToSp", true, null); + } + + private static void SaveReplay() + { + const string suffix = "-preconvert"; + var saveName = $"{GenFile.SanitizedFileName(Multiplayer.session.gameName)}{suffix}"; + MultiplayerSession.SaveGameToFile_Overwrite(saveName, false); + } + + private static void PrepareSingleplayer() + { + Find.GameInfo.permadeathMode = false; + } + + private static void PrepareLoading() + { + Multiplayer.StopMultiplayer(); + + var doc = SaveLoad.SaveGameToDoc(); + MemoryUtility.ClearAllMapsAndWorld(); + + Current.Game = new Game + { + InitData = new GameInitData + { + gameToLoad = "play" + } + }; + + LoadPatch.gameToLoad = new TempGameData(doc, new byte[0]); + } +} diff --git a/Source/Client/Saving/CrossRefs.cs b/Source/Client/Saving/CrossRefs.cs index 65f24cec..0e7b7906 100644 --- a/Source/Client/Saving/CrossRefs.cs +++ b/Source/Client/Saving/CrossRefs.cs @@ -254,7 +254,7 @@ static void Postfix(Thing item) { if (Multiplayer.game == null) return; - if (item.def.HasThingIDNumber) + if (item.def.HasThingIDNumber && item.thingIDNumber >= 0) { ScribeUtil.sharedCrossRefs.RegisterLoaded(item); ThingsById.Register(item); @@ -293,7 +293,7 @@ static void Postfix(ThingOwner __instance) // Ignore null values and minified things with null inner thing. // Since this method is called before ThingOwner<>.ExposeData, // we're using data before it was cleaned up. - if (item != null && item is not MinifiedThing { InnerThing: null }) + if (item != null && item is not MinifiedThing { InnerThing: null } && item.thingIDNumber >= 0) { ScribeUtil.sharedCrossRefs.RegisterLoaded(item); ThingsById.Register(item); diff --git a/Source/Client/Saving/Loader.cs b/Source/Client/Saving/Loader.cs index 83dc5f18..6b479048 100644 --- a/Source/Client/Saving/Loader.cs +++ b/Source/Client/Saving/Loader.cs @@ -58,9 +58,15 @@ private static void PostLoad(bool forceAsyncTime) Multiplayer.session.replayTimerStart = TickPatch.Timer; - Multiplayer.game.ChangeRealPlayerFaction(Find.FactionManager.GetById(Multiplayer.session.myFactionId)); + Multiplayer.game.ChangeRealPlayerFaction( + Find.FactionManager.GetById(Multiplayer.session.myFactionId) ?? Multiplayer.WorldComp.spectatorFaction + ); Multiplayer.game.myFactionLoading = null; + // todo temporary + // Current.Game.InitData = new GameInitData() { mapSize = 50 }; + // Find.WindowStack.Add(new Page_SelectStartingSite()); + if (forceAsyncTime) Multiplayer.game.gameComp.asyncTime = true; @@ -81,8 +87,9 @@ private static XmlDocument DataSnapshotToXml(GameDataSnapshot dataSnapshot, List XmlNode mapNode = gameDoc.ReadNode(reader); gameNode["maps"].AppendChild(mapNode); - if (gameNode["currentMapIndex"] == null) - gameNode.AddNode("currentMapIndex", map.ToString()); + // todo temporary + // if (gameNode["currentMapIndex"] == null) + // gameNode.AddNode("currentMapIndex", map.ToString()); } return gameDoc; diff --git a/Source/Client/Saving/Replay.cs b/Source/Client/Saving/Replay.cs index 1a7c8052..1415c832 100644 --- a/Source/Client/Saving/Replay.cs +++ b/Source/Client/Saving/Replay.cs @@ -103,13 +103,13 @@ public GameDataSnapshot LoadGameData(int sectionId) ); } - public static FileInfo ReplayFile(string fileName, string folder = null) + public static FileInfo SavedReplayFile(string fileName, string folder = null) => new(Path.Combine(folder ?? Multiplayer.ReplaysDir, $"{fileName}.zip")); - public static Replay ForLoading(string fileName) => ForLoading(ReplayFile(fileName)); - public static Replay ForLoading(FileInfo file) => new Replay(file); + public static Replay ForLoading(string fileName) => ForLoading(SavedReplayFile(fileName)); + public static Replay ForLoading(FileInfo file) => new(file); - public static Replay ForSaving(string fileName) => ForSaving(ReplayFile(fileName)); + public static Replay ForSaving(string fileName) => ForSaving(SavedReplayFile(fileName)); public static Replay ForSaving(FileInfo file) { var replay = new Replay(file) @@ -123,13 +123,14 @@ public static Replay ForSaving(FileInfo file) modIds = LoadedModManager.RunningModsListForReading.Select(m => m.PackageId).ToList(), modNames = LoadedModManager.RunningModsListForReading.Select(m => m.Name).ToList(), asyncTime = Multiplayer.GameComp.asyncTime, + multifaction = Multiplayer.GameComp.multifaction } }; return replay; } - public static void LoadReplay(FileInfo file, bool toEnd = false, Action after = null, Action cancel = null, string simTextKey = null) + public static void LoadReplay(FileInfo file, bool toEnd = false, Action after = null, Action cancel = null, string simTextKey = null, bool showTimeline = true) { var session = new MultiplayerSession { @@ -150,6 +151,7 @@ public static void LoadReplay(FileInfo file, bool toEnd = false, Action after = session.myFactionId = replay.info.playerFaction; session.replayTimerStart = replay.info.sections[sectionIndex].start; + session.showTimeline = showTimeline; int tickUntil = replay.info.sections[sectionIndex].end; session.replayTimerEnd = tickUntil; diff --git a/Source/Client/Saving/ReplayConnection.cs b/Source/Client/Saving/ReplayConnection.cs index 6de3ead1..18dff298 100644 --- a/Source/Client/Saving/ReplayConnection.cs +++ b/Source/Client/Saving/ReplayConnection.cs @@ -1,9 +1,27 @@ -using Multiplayer.Common; +using System; +using Multiplayer.Common; namespace Multiplayer.Client; public class ReplayConnection : ConnectionBase { + public static Action replayCmdEvent; + + public override void Send(Packets id, byte[] message, bool reliable = true) + { + if (id == Packets.Client_Command) + replayCmdEvent?.Invoke(DeserializeCmd(new ByteReader(message))); + } + + private static ScheduledCommand DeserializeCmd(ByteReader data) + { + CommandType cmd = (CommandType)data.ReadInt32(); + int mapId = data.ReadInt32(); + byte[] extraBytes = data.ReadPrefixedBytes()!; + + return new ScheduledCommand(cmd, 0, 0, mapId, 0, extraBytes); + } + protected override void SendRaw(byte[] raw, bool reliable) { } diff --git a/Source/Client/Saving/SavingPatches.cs b/Source/Client/Saving/SavingPatches.cs index 9379037c..9ab6cc37 100644 --- a/Source/Client/Saving/SavingPatches.cs +++ b/Source/Client/Saving/SavingPatches.cs @@ -1,6 +1,11 @@ -using HarmonyLib; +using System; +using System.Linq; +using System.Reflection; +using HarmonyLib; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Common; +using RimWorld; using RimWorld.Planet; using Verse; using Verse.Profile; @@ -19,15 +24,40 @@ static void Prefix(Game __instance) if (Scribe.mode is LoadSaveMode.LoadingVars or LoadSaveMode.Saving) { - Scribe_Deep.Look(ref Multiplayer.game.gameComp, "mpGameComp", __instance); + Scribe_Deep.Look(ref Multiplayer.game.gameComp, "mpGameComp"); if (Multiplayer.game.gameComp == null) { Log.Warning($"No {nameof(MultiplayerGameComp)} during loading/saving"); - Multiplayer.game.gameComp = new MultiplayerGameComp(__instance); + Multiplayer.game.gameComp = new MultiplayerGameComp(); } } } + + static void Postfix() + { + if (Multiplayer.Client == null) return; + + // Convert old id blocks to vanilla unique id manager ids + if (Scribe.mode is LoadSaveMode.LoadingVars && Multiplayer.GameComp.idBlockBase64 != null) + { + Log.Message("Multiplayer removing old id block..."); + + var reader = new ByteReader(Convert.FromBase64String(Multiplayer.GameComp.idBlockBase64)); + var (blockStart, _, _, currentInBlock) = (reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32()); + SetAllUniqueIds(blockStart + currentInBlock + 1); + + Multiplayer.GameComp.idBlockBase64 = null; + } + } + + private static void SetAllUniqueIds(int value) + { + typeof(UniqueIDsManager) + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => f.FieldType == typeof(int)) + .Do(f => f.SetValue(Find.UniqueIDsManager, value)); + } } [HarmonyPatch(typeof(MemoryUtility), nameof(MemoryUtility.ClearAllMapsAndWorld))] diff --git a/Source/Client/Saving/Scribe_Custom.cs b/Source/Client/Saving/Scribe_Custom.cs index 6b074749..c348bc31 100644 --- a/Source/Client/Saving/Scribe_Custom.cs +++ b/Source/Client/Saving/Scribe_Custom.cs @@ -1,4 +1,3 @@ -using Multiplayer.Common; using System; using System.Collections.Generic; using UnityEngine; @@ -33,31 +32,6 @@ public static void LookRect(ref Rect rect, string label) rect = new Rect(value.x, value.y, value.z, value.w); } - public static void LookIdBlock(ref IdBlock block, string label) - { - if (Scribe.mode == LoadSaveMode.Saving && block != null) - { - string base64 = Convert.ToBase64String(block.Serialize()); - Scribe_Values.Look(ref base64, label); - } - - if (Scribe.mode == LoadSaveMode.LoadingVars) - { - string base64 = null; - Scribe_Values.Look(ref base64, label); - - if (base64 != null) - block = IdBlock.Deserialize(new ByteReader(Convert.FromBase64String(base64))); - else - block = null; - } - } - - public static void LookRecord(ref T record) - { - - } - // Copy of RimWorld's method but with ctor args public static void LookValueDeep(ref Dictionary dict, string label, params object[] valueCtorArgs) { diff --git a/Source/Client/Settings/MpSettings.cs b/Source/Client/Settings/MpSettings.cs index 49d2cca9..d149c716 100644 --- a/Source/Client/Settings/MpSettings.cs +++ b/Source/Client/Settings/MpSettings.cs @@ -42,7 +42,7 @@ public class MpSettings : ModSettings }; private ServerSettingsClient serverSettingsClient = new(); - public ServerSettings ServerSettings => serverSettingsClient.settings; + public ServerSettings PreferredLocalServerSettings => serverSettingsClient.settings; public override void ExposeData() { diff --git a/Source/Client/Syncing/DefSerialization.cs b/Source/Client/Syncing/DefSerialization.cs index 7adf16a5..42f6e1f2 100644 --- a/Source/Client/Syncing/DefSerialization.cs +++ b/Source/Client/Syncing/DefSerialization.cs @@ -31,7 +31,7 @@ public static void Init() public static Def GetDef(Type defType, ushort hash) { - return (Def)methodCache.AddOrGet( + return (Def)methodCache.GetOrAdd( hashableType[defType], static t => typeof(DefDatabase<>).MakeGenericType(t).GetMethod("GetByShortHash") ).Invoke(null, new[] { (object)hash }); diff --git a/Source/Client/Syncing/DelegateSerialization.cs b/Source/Client/Syncing/DelegateSerialization.cs new file mode 100644 index 00000000..a354160e --- /dev/null +++ b/Source/Client/Syncing/DelegateSerialization.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Multiplayer.Common; +using RimWorld; +using Verse; +using Verse.AI.Group; + +namespace Multiplayer.Client; + +public static class DelegateSerialization +{ + public static void WriteDelegate(ByteWriter writer, Delegate del) + { + writer.WriteBool(del != null); + if (del == null) return; + + SyncSerialization.WriteSync(writer, del.GetType()); + SyncSerialization.WriteSync(writer, del.Method.DeclaringType); + SyncSerialization.WriteSync(writer, del.Method.Name); // todo Handle the signature for ambiguous methods + + writer.WriteBool(del.Target != null); + if (del.Target != null) + { + var targetType = del.Target.GetType(); + SyncSerialization.WriteSync(writer, targetType); + + var fieldPaths = GetFields(targetType).ToArray(); + var fieldTypes = fieldPaths.Select(MpReflection.PathType).ToArray(); + + void SyncObj(object obj, Type type, string debugInfo) + { + if (type.IsCompilerGenerated()) + return; + + (writer as LoggingByteWriter)?.Log.Enter(debugInfo); + + try + { + if (typeof(Delegate).IsAssignableFrom(type)) + WriteDelegate(writer, (Delegate)obj); + else + SyncSerialization.WriteSyncObject(writer, obj, type); + } + finally + { + (writer as LoggingByteWriter)?.Log.Exit(); + } + } + + for (int i = 0; i < fieldPaths.Length; i++) + SyncObj(del.Target.GetPropertyOrField(fieldPaths[i]), fieldTypes[i], fieldPaths[i]); + } + } + + public static Delegate ReadDelegate(ByteReader reader) + { + if (!reader.ReadBool()) return null; + + var delegateType = SyncSerialization.ReadSync(reader); + var type = SyncSerialization.ReadSync(reader); + var methodName = SyncSerialization.ReadSync(reader); + object target = null; + + if (reader.ReadBool()) + { + var targetType = SyncSerialization.ReadSync(reader); + var fieldPaths = GetFields(targetType).ToArray(); + var fieldTypes = fieldPaths.Select(path => MpReflection.PathType(path)).ToArray(); + + target = Activator.CreateInstance(targetType); + + for (int i = 0; i < fieldPaths.Length; i++) + { + string path = fieldPaths[i]; + string noTypePath = MpReflection.RemoveType(path); + Type fieldType = fieldTypes[i]; + object value; + + if (fieldType.IsCompilerGenerated()) + value = Activator.CreateInstance(fieldType); + else if (typeof(Delegate).IsAssignableFrom(fieldType)) + value = ReadDelegate(reader); + else + value = SyncSerialization.ReadSyncObject(reader, fieldType); + + MpReflection.SetValue(target, path, value); + } + } + + return Delegate.CreateDelegate( + delegateType, + target, + CheckMethodAllowed(AccessTools.Method(type, methodName)) + ); + } + + const string CachedLambda = "<>9"; + + private static List GetFields(Type targetType) + { + var fieldList = new List(); + SyncDelegate.AllDelegateFieldsRecursive( + targetType, + path => { if (!path.Contains(CachedLambda)) fieldList.Add(path); return false; }, + allowDelegates: true + ); + + return fieldList; + } + + private static Type[] allowedDeclaringTypes = + { + // For Dialog_BeginRitual.action + typeof(Ability), + typeof(AbilityComp), + typeof(Command), + typeof(ThingComp), + typeof(Dialog_BeginRitual), + typeof(LordToil), + typeof(Precept), + typeof(SocialCardUtility), + + // For DiaOption.action + typeof(Letter), + typeof(FactionDialogMaker), + typeof(GenGameEnd), + typeof(IncidentWorker), + typeof(QuestPart), + typeof(ResearchManager), + typeof(ShipUtility), + }; + + private static bool IsDeclaringTypeAllowed(Type declaringType) + { + while (declaringType.DeclaringType != null) + declaringType = declaringType.DeclaringType; + + do + { + if (allowedDeclaringTypes.Contains(declaringType)) + return true; + declaringType = declaringType.BaseType; + } while (declaringType != null); + + return false; + } + + public static MethodInfo CheckMethodAllowed(MethodInfo method) + { + if (IsDeclaringTypeAllowed(method.DeclaringType)) + return method; + + throw new Exception($"Delegate deserialization: method not allowed {method.MethodDesc()}"); + } +} diff --git a/Source/Client/Syncing/ExposableSerialization.cs b/Source/Client/Syncing/ExposableSerialization.cs index dd3726e9..77ef5ff3 100644 --- a/Source/Client/Syncing/ExposableSerialization.cs +++ b/Source/Client/Syncing/ExposableSerialization.cs @@ -16,7 +16,7 @@ public static class ExposableSerialization public static IExposable ReadExposable(Type type, byte[] data) { - return (IExposable)readExposableCache.AddOrGet(type, newType => ReadExposableDefinition.MakeGenericMethod(newType)).Invoke(null, new[] { (object)data, null }); + return (IExposable)readExposableCache.GetOrAdd(type, newType => ReadExposableDefinition.MakeGenericMethod(newType)).Invoke(null, new[] { (object)data, null }); } } } diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index f4d9d7b2..821c763c 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -52,10 +52,8 @@ public static void Init() SyncDelegate.Lambda(typeof(CompTargetable), nameof(CompTargetable.SelectedUseOption), 0); // Use targetable SyncDelegate.LambdaInGetter(typeof(Designator), nameof(Designator.RightClickFloatMenuOptions), 0) // Designate all - .TransformField("things", Serializer.SimpleReader(() => Find.CurrentMap.listerThings.AllThings)) - .SetContext(SyncContext.CurrentMap); - SyncDelegate.LambdaInGetter(typeof(Designator), nameof(Designator.RightClickFloatMenuOptions), 1) // Remove all designations - .SetContext(SyncContext.CurrentMap); + .TransformField("things", Serializer.SimpleReader(() => Find.CurrentMap.listerThings.AllThings)).SetContext(SyncContext.CurrentMap); + SyncDelegate.LambdaInGetter(typeof(Designator), nameof(Designator.RightClickFloatMenuOptions), 1).SetContext(SyncContext.CurrentMap); // Remove all designations SyncDelegate.Lambda(typeof(CaravanAbandonOrBanishUtility), nameof(CaravanAbandonOrBanishUtility.TryAbandonOrBanishViaInterface), 1, new[] { typeof(Thing), typeof(Caravan) }).CancelIfAnyFieldNull(); // Abandon caravan thing SyncDelegate.Lambda(typeof(CaravanAbandonOrBanishUtility), nameof(CaravanAbandonOrBanishUtility.TryAbandonOrBanishViaInterface), 0, new[] { typeof(TransferableImmutable), typeof(Caravan) }).CancelIfAnyFieldNull(); // Abandon caravan transferable @@ -75,6 +73,7 @@ public static void Init() SyncMethod.Lambda(typeof(CompRefuelable), nameof(CompRefuelable.CompGetGizmosExtra), 5).SetDebugOnly(); // Set fuel to max SyncMethod.Lambda(typeof(CompShuttle), nameof(CompShuttle.CompGetGizmosExtra), 1); // Toggle autoload + SyncMethod.Lambda(typeof(ShipJob_Wait), nameof(ShipJob_Wait.GetJobGizmos), 1); // Send shuttle SyncDelegate.LocalFunc(typeof(RoyalTitlePermitWorker_CallShuttle), nameof(RoyalTitlePermitWorker_CallShuttle.CallShuttleToCaravan), "Launch").ExposeParameter(1); // Call shuttle permit on caravan @@ -228,7 +227,7 @@ The UI's main interaction area is split into three types of groups of pawns. var RitualRolesSerializer = Serializer.New( (IEnumerable roles, object target, object[] args) => { - var dialog = target.GetPropertyOrField(SyncDelegate.DELEGATE_THIS) as Dialog_BeginRitual; + var dialog = target.GetPropertyOrField(SyncDelegate.DelegateThis) as Dialog_BeginRitual; var ids = from r in roles select r.id; return (dialog.ritual.behavior.def, ids); }, @@ -504,7 +503,7 @@ static IEnumerable HumanEmbryoTranspiler(IEnumerable children = new List(); + public List children = new(); public string text; public bool expand; diff --git a/Source/Client/Syncing/Sync.cs b/Source/Client/Syncing/Sync.cs index b9517202..d50e81e4 100644 --- a/Source/Client/Syncing/Sync.cs +++ b/Source/Client/Syncing/Sync.cs @@ -147,7 +147,7 @@ internal static void RegisterAllAttributes(Assembly asm) } catch (Exception e) { - Log.Error($"Exception registering SyncMethod by attribute: {e}"); + Log.Error($"Exception registering SyncMethod {type}::{method} by attribute: {e}"); Multiplayer.loadingErrors = true; } } @@ -173,17 +173,13 @@ private static void RegisterSyncMethod(MethodInfo method, SyncMethodAttribute at int[] exposeParameters = attribute.exposeParameters; int paramNum = method.GetParameters().Length; - if (exposeParameters != null) { - if (exposeParameters.Length != paramNum) { - Log.Error($"Failed to register a method: Invalid number of parameters to expose in SyncMethod attribute applied to {method.DeclaringType.FullName}::{method}. Expected {paramNum}, got {exposeParameters.Length}"); - return; - } else if (exposeParameters.Any(p => p < 0 || p >= paramNum)) { - Log.Error($"Failed to register a method: One or more indexes of parameters to expose in SyncMethod attribute applied to {method.DeclaringType.FullName}::{method} is invalid."); - return; - } + if (exposeParameters != null && exposeParameters.Any(p => p < 0 || p >= paramNum)) + { + Log.Error($"Failed to register a method: One or more indexes of parameters to expose in SyncMethod attribute applied to {method.DeclaringType.FullName}::{method} is invalid."); + return; } - var sm = RegisterSyncMethod(method, (SyncType[]) null); + var sm = RegisterSyncMethod(method); sm.context = attribute.context; sm.debugOnly = attribute.debugOnly; diff --git a/Source/Client/UI/AlertPing.cs b/Source/Client/UI/AlertPing.cs index 54ebb034..c1322d87 100644 --- a/Source/Client/UI/AlertPing.cs +++ b/Source/Client/UI/AlertPing.cs @@ -35,7 +35,7 @@ public override TaggedString GetExplanation() if (Multiplayer.Client == null) return ""; - var players = Multiplayer.session.cursorAndPing.pings.Select(p => p.PlayerInfo?.username).AllNotNull().JoinStringsAtMost(); + var players = Multiplayer.session.locationPings.pings.Select(p => p.PlayerInfo?.username).AllNotNull().JoinStringsAtMost(); return $"{"MpAlertPingDesc1".Translate(players)}\n\n{"MpAlertPingDesc2".Translate()}"; } @@ -47,8 +47,8 @@ private List Culprits { culpritList.Clear(); - if (Multiplayer.Client != null && !Multiplayer.session.cursorAndPing.alertHidden) - foreach (var ping in Multiplayer.session.cursorAndPing.pings) + if (Multiplayer.Client != null && !Multiplayer.session.locationPings.alertHidden) + foreach (var ping in Multiplayer.session.locationPings.pings) { if (ping.PlayerInfo == null) continue; if (ping.Target.HasValue) @@ -67,7 +67,7 @@ public override AlertReport GetReport() public override void OnClick() { if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) - Multiplayer.session.cursorAndPing.alertHidden = true; + Multiplayer.session.locationPings.alertHidden = true; else base.OnClick(); } diff --git a/Source/Client/UI/CursorAndPing.cs b/Source/Client/UI/CursorAndPing.cs deleted file mode 100644 index 882fb701..00000000 --- a/Source/Client/UI/CursorAndPing.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Multiplayer.Client.Util; -using Multiplayer.Common; -using RimWorld; -using RimWorld.Planet; -using UnityEngine; -using Verse; -using Verse.Sound; - -namespace Multiplayer.Client -{ - public class CursorAndPing - { - public List pings = new(); - public bool alertHidden; - private int pingJumpCycle; - - public void UpdatePing() - { - var pingsEnabled = !TickPatch.Simulating && Multiplayer.settings.enablePings; - - if (pingsEnabled) - if (MultiplayerStatic.PingKeyDef.JustPressed || KeyDown(Multiplayer.settings.sendPingButton)) - { - if (WorldRendererUtility.WorldRenderedNow) - PingLocation(-1, GenWorld.MouseTile(), Vector3.zero); - else if (Find.CurrentMap != null) - PingLocation(Find.CurrentMap.uniqueID, 0, UI.MouseMapPosition()); - } - - for (int i = pings.Count - 1; i >= 0; i--) - { - var ping = pings[i]; - - if (ping.Update() || ping.PlayerInfo == null || ping.Target == null) - pings.RemoveAt(i); - } - - if (pingsEnabled && KeyDown(Multiplayer.settings.jumpToPingButton)) - { - pingJumpCycle++; - - if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) - alertHidden = true; - else if (pings.Any()) - // ReSharper disable once PossibleInvalidOperationException - CameraJumper.TryJumpAndSelect(pings[GenMath.PositiveMod(pingJumpCycle, pings.Count)].Target.Value); - } - } - - private static bool KeyDown(KeyCode? keyNullable) - { - if (keyNullable is not { } key) return false; - - if (keyNullable == KeyCode.Mouse2) - return MpInput.Mouse2UpWithoutDrag; - - return Input.GetKeyDown(key); - } - - private void PingLocation(int map, int tile, Vector3 loc) - { - var writer = new ByteWriter(); - writer.WriteInt32(map); - writer.WriteInt32(tile); - writer.WriteFloat(loc.x); - writer.WriteFloat(loc.y); - writer.WriteFloat(loc.z); - Multiplayer.Client.Send(Packets.Client_PingLocation, writer.ToArray()); - - SoundDefOf.TinyBell.PlayOneShotOnCamera(); - } - - public void ReceivePing(int player, int map, int tile, Vector3 loc) - { - if (!Multiplayer.settings.enablePings) return; - - pings.RemoveAll(p => p.player == player); - pings.Add(new PingInfo { player = player, mapId = map, planetTile = tile, mapLoc = loc }); - alertHidden = false; - - if (player != Multiplayer.session.playerId) - SoundDefOf.TinyBell.PlayOneShotOnCamera(); - } - - public void SendVisuals() - { - if (Time.realtimeSinceStartup - lastCursorSend > 0.05f) - { - lastCursorSend = Time.realtimeSinceStartup; - SendCursor(); - } - - if (Time.realtimeSinceStartup - lastSelectedSend > 0.2f) - { - lastSelectedSend = Time.realtimeSinceStartup; - SendSelected(); - } - } - - private byte cursorSeq; - private float lastCursorSend; - - private void SendCursor() - { - var writer = new ByteWriter(); - writer.WriteByte(cursorSeq++); - - if (Find.CurrentMap != null && !WorldRendererUtility.WorldRenderedNow) - { - writer.WriteByte((byte)Find.CurrentMap.Index); - - var icon = Find.MapUI?.designatorManager?.SelectedDesignator?.icon; - int iconId = icon == null ? 0 : !MultiplayerData.icons.Contains(icon) ? 0 : MultiplayerData.icons.IndexOf(icon); - writer.WriteByte((byte)iconId); - - writer.WriteVectorXZ(UI.MouseMapPosition()); - - if (Find.Selector.dragBox.IsValidAndActive) - writer.WriteVectorXZ(Find.Selector.dragBox.start); - else - writer.WriteShort(-1); - } - else - { - writer.WriteByte(byte.MaxValue); - } - - Multiplayer.Client.Send(Packets.Client_Cursor, writer.ToArray(), reliable: false); - } - - private HashSet lastSelected = new(); - private float lastSelectedSend; - private int lastMap; - - private void SendSelected() - { - if (Current.ProgramState != ProgramState.Playing) return; - - var writer = new ByteWriter(); - - int mapId = Find.CurrentMap?.Index ?? -1; - if (WorldRendererUtility.WorldRenderedNow) mapId = -1; - - bool reset = false; - - if (mapId != lastMap) - { - reset = true; - lastMap = mapId; - lastSelected.Clear(); - } - - var selected = new HashSet(Find.Selector.selected.OfType().Select(t => t.thingIDNumber)); - - var add = new List(selected.Except(lastSelected)); - var remove = new List(lastSelected.Except(selected)); - - if (!reset && add.Count == 0 && remove.Count == 0) return; - - writer.WriteBool(reset); - writer.WritePrefixedInts(add); - writer.WritePrefixedInts(remove); - - lastSelected = selected; - - Multiplayer.Client.Send(Packets.Client_Selected, writer.ToArray()); - } - } - - public class PingInfo - { - public int player; - public int mapId; // Map id or -1 for planet - public int planetTile; - public Vector3 mapLoc; - - public PlayerInfo PlayerInfo => Multiplayer.session.GetPlayerInfo(player); - - public float y = 1f; - private float v = -3f; - private float lastTime = Time.time; - private float bounceAt = Time.time + 2; - public float timeAlive; - - private float AlphaMult => 1f - Mathf.Clamp01(timeAlive - (PingDuration - 1f)); - - public GlobalTargetInfo? Target - { - get - { - if (mapId == -1) - return new GlobalTargetInfo(planetTile); - - if (Find.Maps.GetById(mapId) is { } map) - return new GlobalTargetInfo(mapLoc.ToIntVec3(), map); - - return null; - } - } - - const float PingDuration = 10f; - - public bool Update() - { - float delta = Mathf.Min(Time.time - lastTime, 0.05f); - lastTime = Time.time; - - v -= 8f * delta; - y += v * delta; - - if (y < 0) - { - y = 0; - v = Math.Max(-v / 2f - 0.5f, 0); - } - - if (Mathf.Abs(v) < 0.0001f && y < 0.05f) - { - v = 0f; - y = 0f; - } - - if (bounceAt != 0 && Time.time > bounceAt) - { - v = 3f; - bounceAt = Time.time + 2; - } - - timeAlive += delta; - - return timeAlive > PingDuration; - } - - public void DrawAt(Vector2 screenCenter, Color baseColor, float size) - { - var colorAlpha = baseColor; - colorAlpha.a = 0.5f * AlphaMult; - - using (MpStyle.Set(colorAlpha)) - GUI.DrawTexture( - new Rect(screenCenter - new Vector2(size / 2 - 1, size / 2), new(size, size)), - MultiplayerStatic.PingBase - ); - - var color = baseColor; - color.a = AlphaMult; - - using (MpStyle.Set(color)) - GUI.DrawTexture( - new Rect(screenCenter - new Vector2(size / 2, size + y * size), new(size, size)), - MultiplayerStatic.PingPin - ); - } - } -} diff --git a/Source/Client/UI/CursorPatches.cs b/Source/Client/UI/CursorPatches.cs new file mode 100644 index 00000000..49fd2081 --- /dev/null +++ b/Source/Client/UI/CursorPatches.cs @@ -0,0 +1,134 @@ +using HarmonyLib; +using Multiplayer.Common; +using RimWorld; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client +{ + [HarmonyPatch(typeof(Targeter), nameof(Targeter.TargeterOnGUI))] + static class DrawPlayerCursors + { + static void Postfix() + { + if (Multiplayer.Client == null || !Multiplayer.settings.showCursors || TickPatch.Simulating) return; + + var curMap = Find.CurrentMap.Index; + + foreach (var player in Multiplayer.session.players) + { + if (player.username == Multiplayer.username) continue; + if (player.map != curMap) continue; + if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; + + if (Multiplayer.settings.transparentPlayerCursors) + GUI.color = player.color * new Color(1, 1, 1, 0.5f); + else + GUI.color = player.color * new Color(1, 1, 1, 1); + + var pos = Vector3.Lerp( + player.lastCursor, + player.cursor, + (float)(Multiplayer.clock.ElapsedMillisDouble() - player.updatedAt) / 50f + ).MapToUIPosition(); + + var icon = MultiplayerData.icons.ElementAtOrDefault(player.cursorIcon); + var drawIcon = icon ?? CustomCursor.CursorTex; + var iconRect = new Rect(pos, new Vector2(24f * drawIcon.width / drawIcon.height, 24f)); + + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(new Rect(pos, new Vector2(100, 30)).CenterOn(iconRect).Down(20f).Left(icon != null ? 0f : 5f), player.username); + Text.Anchor = TextAnchor.UpperLeft; + Text.Font = GameFont.Small; + + if (icon != null && MultiplayerData.iconInfos[player.cursorIcon].hasStuff) + GUI.color = new Color(0.5f, 0.4f, 0.26f, 0.5f); // Stuff color for wood + + GUI.DrawTexture(iconRect, drawIcon); + + if (player.dragStart != PlayerInfo.Invalid) + { + GUI.color = player.color * new Color(1, 1, 1, 0.2f); + Widgets.DrawBox(new Rect() { min = player.dragStart.MapToUIPosition(), max = pos }, 2); + } + + GUI.color = Color.white; + } + } + } + + [HarmonyPatch(typeof(SelectionDrawer), nameof(SelectionDrawer.DrawSelectionOverlays))] + [StaticConstructorOnStartup] + static class SelectionBoxPatch + { + static Material graySelection = MaterialPool.MatFrom("UI/Overlays/SelectionBracket", ShaderDatabase.MetaOverlay); + static MaterialPropertyBlock propBlock = new MaterialPropertyBlock(); + static HashSet drawnThisUpdate = new HashSet(); + static Dictionary selTimes = new Dictionary(); + + static void Postfix() + { + if (Multiplayer.Client == null || TickPatch.Simulating) return; + + foreach (var t in Find.Selector.SelectedObjects.OfType()) + drawnThisUpdate.Add(t.thingIDNumber); + + foreach (var player in Multiplayer.session.players) + { + if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; + + foreach (var sel in player.selectedThings) + { + if (!drawnThisUpdate.Add(sel.Key)) continue; + if (!ThingsById.thingsById.TryGetValue(sel.Key, out Thing thing)) continue; + if (thing.Map != Find.CurrentMap) continue; + + selTimes[thing] = sel.Value; + SelectionDrawerUtility.CalculateSelectionBracketPositionsWorld(SelectionDrawer.bracketLocs, thing, thing.DrawPos, thing.RotatedSize.ToVector2(), selTimes, Vector2.one, 1f); + selTimes.Clear(); + + for (int i = 0; i < 4; i++) + { + Quaternion rotation = Quaternion.AngleAxis(-i * 90, Vector3.up); + propBlock.SetColor("_Color", player.color * new Color(1, 1, 1, 0.5f)); + Graphics.DrawMesh(MeshPool.plane10, SelectionDrawer.bracketLocs[i], rotation, graySelection, 0, null, 0, propBlock); + } + } + } + + drawnThisUpdate.Clear(); + } + } + + [HarmonyPatch(typeof(InspectPaneFiller), nameof(InspectPaneFiller.DrawInspectStringFor))] + static class DrawInspectPaneStringMarker + { + public static ISelectable drawingFor; + + static void Prefix(ISelectable sel) => drawingFor = sel; + static void Postfix() => drawingFor = null; + } + + [HarmonyPatch(typeof(InspectPaneFiller), nameof(InspectPaneFiller.DrawInspectString))] + static class DrawInspectStringPatch + { + static void Prefix(ref string str) + { + if (Multiplayer.Client == null) return; + if (!(DrawInspectPaneStringMarker.drawingFor is Thing thing)) return; + + List players = new List(); + + foreach (var player in Multiplayer.session.players) + if (player.selectedThings.ContainsKey(thing.thingIDNumber)) + players.Add(player.username); + + if (players.Count > 0) + str += $"\nSelected by: {players.Join()}"; + } + } + +} diff --git a/Source/Client/UI/DrawPingMap.cs b/Source/Client/UI/DrawPingMap.cs index 86911551..5502f1f7 100644 --- a/Source/Client/UI/DrawPingMap.cs +++ b/Source/Client/UI/DrawPingMap.cs @@ -14,7 +14,7 @@ static void Postfix() var size = Math.Min(UI.CurUICellSize() * 4, 32f); - foreach (var ping in Multiplayer.session.cursorAndPing.pings) + foreach (var ping in Multiplayer.session.locationPings.pings) { if (ping.mapId != Find.CurrentMap.uniqueID) continue; if (ping.PlayerInfo is not { } player) continue; diff --git a/Source/Client/UI/DrawPingPlanet.cs b/Source/Client/UI/DrawPingPlanet.cs index 002e60bd..f226cff7 100644 --- a/Source/Client/UI/DrawPingPlanet.cs +++ b/Source/Client/UI/DrawPingPlanet.cs @@ -12,7 +12,7 @@ static void Postfix() { if (Multiplayer.Client == null || TickPatch.Simulating) return; - foreach (var ping in Multiplayer.session.cursorAndPing.pings) + foreach (var ping in Multiplayer.session.locationPings.pings) { if (ping.mapId != -1) continue; if (ping.PlayerInfo is not { } player) continue; diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index 86d67d8b..7454be25 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -29,8 +29,8 @@ internal static void DoDebugPrintout() int timerLag = (TickPatch.tickUntil - TickPatch.Timer); StringBuilder text = new StringBuilder(); text.Append( - $"{Faction.OfPlayer.loadID} {Multiplayer.RealPlayerFaction?.loadID} {Find.UniqueIDsManager.nextThingID} j:{Find.UniqueIDsManager.nextJobID} {Find.TickManager.TicksGame} {Find.TickManager.CurTimeSpeed} {TickPatch.Timer} {TickPatch.tickUntil} {timerLag}"); - text.Append($"\n{Time.deltaTime * 60f:0.0000} {TickPatch.tickTimer.ElapsedMilliseconds}"); + $"{FactionContext.stack.Count} {Faction.OfPlayer.loadID} {Multiplayer.RealPlayerFaction?.loadID} {Find.UniqueIDsManager.nextThingID} j:{Find.UniqueIDsManager.nextJobID} {Find.TickManager.TicksGame} {Find.TickManager.CurTimeSpeed} {TickPatch.Timer} {TickPatch.tickUntil} {timerLag}"); + text.Append($"\n{1f/Time.deltaTime:0.0000} {TickPatch.tickTimer.ElapsedMilliseconds}"); text.Append($"\n{avgDelta = (avgDelta * 59.0 + Time.deltaTime * 60.0) / 60.0:0.0000}"); text.Append( $"\n{avgTickTime = (avgTickTime * 59.0 + TickPatch.tickTimer.ElapsedMilliseconds) / 60.0:0.0000} {Find.World.worldObjects.settlements.Count}"); @@ -81,7 +81,7 @@ internal static void DoDebugPrintout() text.Append( $"\n{async.cmds.Count} {Multiplayer.AsyncWorldTime.cmds.Count} {async.slower.forceNormalSpeedUntil} {Multiplayer.GameComp.asyncTime}"); text.Append( - $"\nt{DeferredStackTracing.maxTraceDepth} p{SimplePool.FreeItemsCount} {DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize} {DeferredStackTracingImpl.collisions}"); + $"\n{Find.WorldPawns.AllPawnsAliveOrDead.Count} t{DeferredStackTracing.maxTraceDepth} p{SimplePool.FreeItemsCount} {DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize} {DeferredStackTracingImpl.collisions}"); text.Append(Find.WindowStack.focusedWindow is ImmediateWindow win ? $"\nImmediateWindow: {MpUtil.DelegateMethodInfo(win.doWindowFunc?.Method)}" @@ -154,4 +154,22 @@ internal static float DoDebugModeLabel(float y) return 0; } + + internal static float DoTimeDiffLabel(float y) + { + float x = UI.screenWidth - BtnWidth - BtnMargin; + + if (Multiplayer.Client != null && + !Multiplayer.GameComp.asyncTime && + Find.CurrentMap.AsyncTime() != null && + Find.CurrentMap.AsyncTime().mapTicks != Multiplayer.AsyncWorldTime.worldTicks) + { + using (MpStyle.Set(GameFont.Tiny).Set(TextAnchor.MiddleCenter)) + Widgets.Label(new Rect(x, y, BtnWidth, 30f), $"{Find.CurrentMap.AsyncTime().mapTicks - Multiplayer.AsyncWorldTime.worldTicks}"); + + return BtnHeight; + } + + return 0; + } } diff --git a/Source/Client/UI/IngameUI.cs b/Source/Client/UI/IngameUI.cs index f45d53ff..84d0563c 100644 --- a/Source/Client/UI/IngameUI.cs +++ b/Source/Client/UI/IngameUI.cs @@ -5,20 +5,19 @@ using System.Collections.Generic; using UnityEngine; using Verse; -using Multiplayer.Common.Util; using RimWorld.Planet; namespace Multiplayer.Client { [HarmonyPatch(typeof(MainButtonsRoot), nameof(MainButtonsRoot.MainButtonsOnGUI))] - [HotSwappable] public static class IngameUIPatch { public static List> upperLeftDrawers = new() { DoChatAndTicksBehind, IngameDebug.DoDevInfo, - IngameDebug.DoDebugModeLabel + IngameDebug.DoDebugModeLabel, + IngameDebug.DoTimeDiffLabel }; private const float BtnMargin = 8f; @@ -33,7 +32,7 @@ static bool Prefix() IngameDebug.DoDebugPrintout(); } - if (Multiplayer.IsReplay || TickPatch.Simulating) + if (Multiplayer.IsReplay && Multiplayer.session.showTimeline || TickPatch.Simulating) ReplayTimeline.DrawTimeline(); if (TickPatch.Simulating) @@ -114,7 +113,7 @@ private static float DoChatAndTicksBehind(float y) var biggerRect = new Rect(btnRect.x - 25f - 5f + 2f / 2f, btnRect.y + 2f / 2f, 23f, 23f); if (slow && Widgets.ButtonInvisible(biggerRect)) - TickPatch.SetSimulation(toTickUntil: true, canESC: true); + TickPatch.SetSimulation(toTickUntil: true, canEsc: true); Widgets.DrawRectFast(biggerRect, new Color(color.r * 0.6f, color.g * 0.6f, color.b * 0.6f)); Widgets.DrawRectFast(indRect, color); diff --git a/Source/Client/UI/Layouter.cs b/Source/Client/UI/Layouter.cs new file mode 100644 index 00000000..59738080 --- /dev/null +++ b/Source/Client/UI/Layouter.cs @@ -0,0 +1,493 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public static class Layouter +{ + #region Data + private class El + { + public El? nextTopLevel; + + public El? parent; + public List children = new(); + public int currentChild; + public Action? drawer; + + public bool horizontal; + public DimensionMode widthMode, heightMode; + public int stretchChildrenX, stretchChildrenY; + public float aspectRatio; + public float childrenHeight; + public float paddingRight; + public bool scroll; + + public string? stringContent; + public GUIStyle? style; + + public float spacing; + public Rect rect; + + private static int indent; + + public override string ToString() + { + string output = new string(' ', indent) + $"Horizontal:{horizontal} Width:{widthMode},{stretchChildrenX} Height:{heightMode},{stretchChildrenY} Rect:{rect} PaddingRight:{paddingRight}"; + + if (children.Any()) + output += "\n"; + + indent++; + for (int i = 0; i < children.Count; i++) + output += (i != 0 ? "\n" : "") + new string(' ', indent) + children[i]; + indent--; + + return output; + } + } + + enum DimensionMode + { + Fixed, + Stretch, + Decide, // Stretch if there are any Stretch children, otherwise Fixed with max child dimension + AspectRatio, // Only possible for height (height depends on width) + ContentHeight, // Only possible for height (height depends on width) + SumFixedChildren, // Only possible for height + MaxFixedSibling, // Only possible for height + } + + private static El? currentGroup; + + private static Dictionary firstTopLevel = new (); // First element of linked list of areas per window id + private static El? currentTopLevel; + + private static readonly Rect DummyRect = new(0f, 0f, 1f, 1f); + + private const int NoWindowId = int.MaxValue; + #endregion + + #region Main + public static void BeginFrame() + { + if (Event.current.type == EventType.Layout) + firstTopLevel.Clear(); + + currentTopLevel = null; + currentGroup = null; + } + + private static void DoDrawers(El parent) + { + parent.drawer?.Invoke(parent.rect); + + foreach (var el in parent.children) + DoDrawers(el); + } + + public static string DebugString() + { + return firstTopLevel.GetValueOrDefault(Find.WindowStack.focusedWindow.ID)?.ToString() ?? + "No layout for focused window"; + } + + private static El GetNextChild() + { + var child = currentGroup!.children[currentGroup.currentChild++]; + return child; + } + #endregion + + #region Groups + private static void PushGroup(El el) + { + var parent = currentGroup; + currentGroup = el; + + if (parent != null) + { + currentGroup.parent = parent; + parent.children.Add(currentGroup); + } + } + + private static void PopGroup() + { + currentGroup = currentGroup!.parent; + } + + public static bool BeginArea(Rect rect, float spacing = 10f) + { + return BeginArea(Find.WindowStack.currentlyDrawnWindow?.ID ?? NoWindowId, rect, spacing); + } + + public static bool BeginArea(int windowId, Rect rect, float spacing = 10f) + { + if (Event.current.type == EventType.Layout) + { + PushGroup(new El { rect = rect.AtZero(), spacing = spacing }); + + // Add to linked list + if (!firstTopLevel.ContainsKey(windowId)) + firstTopLevel[windowId] = currentGroup; + else + currentTopLevel!.nextTopLevel = currentGroup; + + // Set current list pointer + currentTopLevel = currentGroup; + } + else + { + if (!firstTopLevel.ContainsKey(windowId)) + return false; + + currentTopLevel = currentTopLevel == null ? + firstTopLevel[windowId] : + currentTopLevel.nextTopLevel; + currentGroup = currentTopLevel; + } + + GUI.BeginGroup(rect); + return true; + } + + public static void EndArea() + { + if (Event.current.type == EventType.Layout) + Layout(); + + PopGroup(); + + // Calling drawers after layout but still within GUI group + DoDrawers(currentTopLevel!); + + if (currentTopLevel?.nextTopLevel == null) + currentTopLevel = null; + + GUI.EndGroup(); + } + + public static void BeginHorizontal(float spacing = 10f) + { + if (Event.current.type == EventType.Layout) + PushGroup(new El { widthMode = DimensionMode.Stretch, heightMode = DimensionMode.Decide, horizontal = true, spacing = spacing}); + else + currentGroup = GetNextChild(); + } + + public static void EndHorizontal() + { + PopGroup(); + } + + public static void BeginVertical(float spacing = 10f, bool stretch = true) + { + if (Event.current.type == EventType.Layout) + PushGroup(new El { widthMode = DimensionMode.Decide, heightMode = stretch ? DimensionMode.Stretch : DimensionMode.SumFixedChildren, horizontal = false, spacing = spacing}); + else + currentGroup = GetNextChild(); + } + + public static void EndVertical() + { + PopGroup(); + } + + public static void BeginScroll(ref Vector2 scrollPos, float spacing = 10f) + { + BeginVertical(spacing); + + var outRect = currentGroup!.rect; + currentGroup.scroll = true; + + Widgets.BeginScrollView( + outRect, + ref scrollPos, + new Rect(0, 0, outRect.width - currentGroup.paddingRight, currentGroup.childrenHeight)); + } + + public static void EndScroll() + { + Widgets.EndScrollView(); + EndVertical(); + } + #endregion + + #region Rects + public static Rect Rect(float width, float height) + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { + rect = new Rect(0, 0, width, height), + heightMode = DimensionMode.Fixed, + widthMode = DimensionMode.Fixed + }); + return DummyRect; + } + + return GetNextChild().rect; + } + + public static Rect AspectRect(float widthByHeight) + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { widthMode = DimensionMode.Stretch, heightMode = DimensionMode.AspectRatio, aspectRatio = widthByHeight }); + return DummyRect; + } + + return GetNextChild().rect; + } + + public static Rect ContentRect(string stringContent) + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { widthMode = DimensionMode.Stretch, heightMode = DimensionMode.ContentHeight, stringContent = stringContent, style = new GUIStyle(Text.CurFontStyle) }); + return DummyRect; + } + + return GetNextChild().rect; + } + + public static Rect FlexibleSpace() + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { widthMode = DimensionMode.Stretch, heightMode = DimensionMode.Stretch }); + return DummyRect; + } + + return GetNextChild().rect; + } + + // Postpones drawing the rect until after it's laid out + // This allows nesting areas + public static void PostponeFlexible(Action drawer) + { + FlexibleSpace(); + + if (Event.current.type == EventType.Layout) + currentGroup!.children.Last().drawer = drawer; + } + + public static Rect FlexibleWidth() + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { widthMode = DimensionMode.Stretch, heightMode = DimensionMode.MaxFixedSibling }); + return DummyRect; + } + + return GetNextChild().rect; + } + + public static Rect FixedWidth(float width) + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { rect = new Rect(0, 0, width, 0), widthMode = DimensionMode.Fixed, heightMode = DimensionMode.MaxFixedSibling }); + return DummyRect; + } + + return GetNextChild().rect; + } + + public static Rect LastRect() + { + return + Event.current.type == EventType.Layout ? + currentGroup!.children.Last().rect : + currentGroup!.children[currentGroup.currentChild - 1].rect; + } + + public static Rect GroupRect() + { + return currentGroup!.rect; + } + #endregion + + #region Algorithm + private static void Layout() + { + BottomUpWidth(currentTopLevel!); + TopDownWidth(currentTopLevel!); + + BottomUpHeight(currentTopLevel!); + TopDownHeight(currentTopLevel!); + + TopDownWidth(currentTopLevel!); // another pass for scroll views + + TopDownPosition(currentTopLevel!); + } + + private static void BottomUpWidth(El parent) + { + foreach (var el in parent.children) + BottomUpWidth(el); + + foreach (var el in parent.children) + { + if (el.widthMode == DimensionMode.Stretch) + { + if (parent.widthMode == DimensionMode.Decide) + parent.widthMode = DimensionMode.Stretch; + parent.stretchChildrenX++; + } + } + + if (parent.widthMode == DimensionMode.Decide) + { + foreach (var el in parent.children) + parent.rect.width = Mathf.Max(parent.rect.width, el.rect.width); + + parent.widthMode = DimensionMode.Fixed; + } + } + + private static void BottomUpHeight(El parent) + { + foreach (var el in parent.children) + BottomUpHeight(el); + + foreach (var el in parent.children) + { + if (el.heightMode == DimensionMode.Stretch) + { + if (parent.heightMode == DimensionMode.Decide) + parent.heightMode = DimensionMode.Stretch; + parent.stretchChildrenY++; + } + } + + if (parent.heightMode == DimensionMode.SumFixedChildren) + { + foreach (var el in parent.children) + parent.rect.height += el.rect.height; + + if (parent.children.Any()) + parent.rect.height += parent.spacing * parent.children.Count - 1; + + parent.heightMode = DimensionMode.Fixed; + } + + if (parent.heightMode == DimensionMode.Decide) + { + foreach (var el in parent.children) + parent.rect.height = Mathf.Max(parent.rect.height, el.rect.height); + + parent.heightMode = DimensionMode.Fixed; + } + } + + private static void TopDownWidth(El parent) + { + if (parent.scroll && parent.childrenHeight > parent.rect.height) + parent.paddingRight = 16f; + + float childrenWidth = parent.horizontal ? (parent.children.Count - 1) * parent.spacing : 0; + + // Collect known child dimensions + foreach (var el in parent.children) + { + if (el.widthMode == DimensionMode.Fixed) + childrenWidth += el.rect.width; + } + + // Size flexible children + foreach (var el in parent.children) + { + if (el.widthMode == DimensionMode.Stretch) + el.rect.width = parent.horizontal && parent.stretchChildrenX > 0 ? + (parent.rect.width - childrenWidth - parent.paddingRight) / parent.stretchChildrenX : + parent.rect.width - parent.paddingRight; + } + + if (parent.heightMode == DimensionMode.AspectRatio) + { + parent.rect.height = parent.rect.width / parent.aspectRatio; + parent.heightMode = DimensionMode.Fixed; + } + + if (parent.heightMode == DimensionMode.ContentHeight) + { + Text.tmpTextGUIContent.text = parent.stringContent.StripTags(); + parent.rect.height = parent.style!.CalcHeight(Text.tmpTextGUIContent, parent.rect.width); + parent.heightMode = DimensionMode.Fixed; + } + + foreach (var el in parent.children) + TopDownWidth(el); + } + + private static void TopDownHeight(El parent) + { + parent.childrenHeight = !parent.horizontal ? (parent.children.Count - 1) * parent.spacing : 0; + var maxChildHeight = 0f; + + // Collect known child dimensions + foreach (var el in parent.children) + { + if (el.heightMode == DimensionMode.Fixed) + { + parent.childrenHeight += el.rect.height; + maxChildHeight = Mathf.Max(maxChildHeight, el.rect.height); + } + } + + // Size flexible children + foreach (var el in parent.children) + { + if (el.heightMode == DimensionMode.Stretch) + el.rect.height += !parent.horizontal && parent.stretchChildrenY > 0 ? + (parent.rect.height - parent.childrenHeight) / parent.stretchChildrenY : + parent.rect.height; + + if (el.heightMode == DimensionMode.MaxFixedSibling) + el.rect.height = maxChildHeight; + } + + foreach (var el in parent.children) + TopDownHeight(el); + } + + private static void TopDownPosition(El parent) + { + float lastPos = parent.horizontal ? parent.rect.x : parent.rect.y; + + // Layout children in sequence + foreach (var el in parent.children) + { + if (parent.horizontal) + { + el.rect.x = lastPos; + el.rect.y = parent.rect.y; + lastPos += el.rect.width; + } + else + { + el.rect.y = lastPos; + el.rect.x = parent.rect.x; + lastPos += el.rect.height; + } + + if (el != parent.children.Last()) + lastPos += parent.spacing; + } + + foreach (var el in parent.children) + TopDownPosition(el); + } + #endregion +} diff --git a/Source/Client/UI/LocationPings.cs b/Source/Client/UI/LocationPings.cs new file mode 100644 index 00000000..3a82e36e --- /dev/null +++ b/Source/Client/UI/LocationPings.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace Multiplayer.Client; + +public class LocationPings +{ + public List pings = new(); + public bool alertHidden; + private int pingJumpCycle; + + public void UpdatePing() + { + var pingsEnabled = !TickPatch.Simulating && Multiplayer.settings.enablePings; + + if (pingsEnabled) + if (MultiplayerStatic.PingKeyDef.JustPressed || KeyDown(Multiplayer.settings.sendPingButton)) + { + if (WorldRendererUtility.WorldRenderedNow) + PingLocation(-1, GenWorld.MouseTile(), Vector3.zero); + else if (Find.CurrentMap != null) + PingLocation(Find.CurrentMap.uniqueID, 0, UI.MouseMapPosition()); + } + + for (int i = pings.Count - 1; i >= 0; i--) + { + var ping = pings[i]; + + if (ping.Update() || ping.PlayerInfo == null || ping.Target == null) + pings.RemoveAt(i); + } + + if (pingsEnabled && KeyDown(Multiplayer.settings.jumpToPingButton)) + { + pingJumpCycle++; + + if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) + alertHidden = true; + else if (pings.Any()) + // ReSharper disable once PossibleInvalidOperationException + CameraJumper.TryJumpAndSelect(pings[GenMath.PositiveMod(pingJumpCycle, pings.Count)].Target.Value); + } + } + + private static bool KeyDown(KeyCode? keyNullable) + { + if (keyNullable is not { } key) return false; + + if (keyNullable == KeyCode.Mouse2) + return MpInput.Mouse2UpWithoutDrag; + + return Input.GetKeyDown(key); + } + + private void PingLocation(int map, int tile, Vector3 loc) + { + var writer = new ByteWriter(); + writer.WriteInt32(map); + writer.WriteInt32(tile); + writer.WriteFloat(loc.x); + writer.WriteFloat(loc.y); + writer.WriteFloat(loc.z); + Multiplayer.Client.Send(Packets.Client_PingLocation, writer.ToArray()); + + SoundDefOf.TinyBell.PlayOneShotOnCamera(); + } + + public void ReceivePing(int player, int map, int tile, Vector3 loc) + { + if (!Multiplayer.settings.enablePings) return; + + pings.RemoveAll(p => p.player == player); + pings.Add(new PingInfo { player = player, mapId = map, planetTile = tile, mapLoc = loc }); + alertHidden = false; + + if (player != Multiplayer.session.playerId) + SoundDefOf.TinyBell.PlayOneShotOnCamera(); + } +} diff --git a/Source/Client/UI/MainMenuAnimation.cs b/Source/Client/UI/MainMenuAnimation.cs index a73b438a..7fa6d3f9 100644 --- a/Source/Client/UI/MainMenuAnimation.cs +++ b/Source/Client/UI/MainMenuAnimation.cs @@ -3,7 +3,6 @@ using System.Linq; using HarmonyLib; using Multiplayer.Client.Util; -using Multiplayer.Common.Util; using RimWorld; using UnityEngine; using Verse; @@ -11,7 +10,6 @@ namespace Multiplayer.Client { - [HotSwappable] [HarmonyPatch(typeof(UI_BackgroundMain), nameof(UI_BackgroundMain.DoOverlay))] [StaticConstructorOnStartup] static class MainMenuAnimation @@ -119,6 +117,9 @@ static void DrawPulses(Rect bgRect) GetPos(bgRect, posX, posY), new(6, 6) ), + // After changing the game language, RimWorld destroys and reloads all assets but doesn't + // rerun static constructors, so resource fields reference destroyed resources + // The pulses disappear after changing the language MultiplayerStatic.Pulse ); } diff --git a/Source/Client/UI/MainMenuPatches.cs b/Source/Client/UI/MainMenuPatches.cs index bdfe8b52..e37838bf 100644 --- a/Source/Client/UI/MainMenuPatches.cs +++ b/Source/Client/UI/MainMenuPatches.cs @@ -3,12 +3,11 @@ using RimWorld; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; +using Multiplayer.Client.Saving; using UnityEngine; using Verse; -using Verse.Profile; namespace Multiplayer.Client { @@ -60,7 +59,7 @@ static void Prefix(Rect rect, List optList) if (MpVersion.IsDebug && Multiplayer.IsReplay) optList.Insert(0, new ListableOption( "MpHostServer".Translate(), - () => Find.WindowStack.Add(new HostWindow(hadSimulation: true) { layer = WindowLayer.Super }) + () => Find.WindowStack.Add(new HostWindow() { layer = WindowLayer.Super }) )); if (Multiplayer.Client != null) @@ -68,7 +67,14 @@ static void Prefix(Rect rect, List optList) optList.RemoveAll(opt => opt.label == "Save".Translate() || opt.label == "LoadGame".Translate()); if (!Multiplayer.IsReplay) { - optList.Insert(0, new ListableOption("Save".Translate(), () => Find.WindowStack.Add(new SaveGameWindow(Multiplayer.session.gameName) { layer = WindowLayer.Super }))); + optList.Insert( + 0, + new ListableOption( + "Save".Translate(), + () => Find.WindowStack.Add(new SaveGameWindow(Multiplayer.session.gameName) + { + layer = WindowLayer.Super + }))); } var quitMenuLabel = "QuitToMainMenu".Translate(); @@ -150,43 +156,10 @@ static string GetServerCloseConfirmation() private static void AskConvertToSingleplayer() { - static void Convert() - { - LongEventHandler.QueueLongEvent(() => - { - try - { - const string suffix = "-preconvert"; - var saveName = $"{GenFile.SanitizedFileName(Multiplayer.session.gameName)}{suffix}"; - - new FileInfo(Path.Combine(Multiplayer.ReplaysDir, saveName + ".zip")).Delete(); - Replay.ForSaving(saveName).WriteCurrentData(); - } - catch (Exception e) - { - Log.Warning($"Convert to singleplayer failed to write pre-convert file: {e}"); - } - - Find.GameInfo.permadeathMode = false; - HostUtil.SetAllUniqueIds(Multiplayer.GlobalIdBlock.Current); - - Multiplayer.StopMultiplayer(); - - var doc = SaveLoad.SaveGameToDoc(); - MemoryUtility.ClearAllMapsAndWorld(); - - Current.Game = new Game(); - Current.Game.InitData = new GameInitData(); - Current.Game.InitData.gameToLoad = "play"; - - LoadPatch.gameToLoad = new TempGameData(doc, Array.Empty()); - }, "Play", "MpConvertingToSp", true, null); - } - Find.WindowStack.Add( Dialog_MessageBox.CreateConfirmation( Multiplayer.LocalServer != null ? "MpConvertToSpWarnHost".Translate() : "MpConvertToSpWarn".Translate(), - Convert, + ConvertToSp.DoConvert, true, layer: WindowLayer.Super ) diff --git a/Source/Client/UI/PingInfo.cs b/Source/Client/UI/PingInfo.cs new file mode 100644 index 00000000..cbeb6d01 --- /dev/null +++ b/Source/Client/UI/PingInfo.cs @@ -0,0 +1,93 @@ +using System; +using Multiplayer.Client.Util; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public class PingInfo +{ + public int player; + public int mapId; // Map id or -1 for planet + public int planetTile; + public Vector3 mapLoc; + + public PlayerInfo PlayerInfo => Multiplayer.session.GetPlayerInfo(player); + + public float y = 1f; + private float v = -3f; + private float lastTime = Time.time; + private float bounceAt = Time.time + 2; + public float timeAlive; + + private float AlphaMult => 1f - Mathf.Clamp01(timeAlive - (PingDuration - 1f)); + + public GlobalTargetInfo? Target + { + get + { + if (mapId == -1) + return new GlobalTargetInfo(planetTile); + + if (Find.Maps.GetById(mapId) is { } map) + return new GlobalTargetInfo(mapLoc.ToIntVec3(), map); + + return null; + } + } + + const float PingDuration = 10f; + + public bool Update() + { + float delta = Mathf.Min(Time.time - lastTime, 0.05f); + lastTime = Time.time; + + v -= 8f * delta; + y += v * delta; + + if (y < 0) + { + y = 0; + v = Math.Max(-v / 2f - 0.5f, 0); + } + + if (Mathf.Abs(v) < 0.0001f && y < 0.05f) + { + v = 0f; + y = 0f; + } + + if (bounceAt != 0 && Time.time > bounceAt) + { + v = 3f; + bounceAt = Time.time + 2; + } + + timeAlive += delta; + + return timeAlive > PingDuration; + } + + public void DrawAt(Vector2 screenCenter, Color baseColor, float size) + { + var colorAlpha = baseColor; + colorAlpha.a = 0.5f * AlphaMult; + + using (MpStyle.Set(colorAlpha)) + GUI.DrawTexture( + new Rect(screenCenter - new Vector2(size / 2 - 1, size / 2), new(size, size)), + MultiplayerStatic.PingBase + ); + + var color = baseColor; + color.a = AlphaMult; + + using (MpStyle.Set(color)) + GUI.DrawTexture( + new Rect(screenCenter - new Vector2(size / 2, size + y * size), new(size, size)), + MultiplayerStatic.PingPin + ); + } +} diff --git a/Source/Client/UI/PlayerCursors.cs b/Source/Client/UI/PlayerCursors.cs index 49fd2081..99b45190 100644 --- a/Source/Client/UI/PlayerCursors.cs +++ b/Source/Client/UI/PlayerCursors.cs @@ -1,134 +1,96 @@ -using HarmonyLib; -using Multiplayer.Common; -using RimWorld; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Multiplayer.Common; +using RimWorld.Planet; using UnityEngine; using Verse; -namespace Multiplayer.Client +namespace Multiplayer.Client; + +public class PlayerCursors { - [HarmonyPatch(typeof(Targeter), nameof(Targeter.TargeterOnGUI))] - static class DrawPlayerCursors + private HashSet lastSelected = new(); + private float lastSelectedSend; + private int lastMap; + + private byte cursorSeq; + private float lastCursorSend; + + public void SendVisuals() { - static void Postfix() + if (Time.realtimeSinceStartup - lastCursorSend > 0.05f) + { + lastCursorSend = Time.realtimeSinceStartup; + SendCursor(); + } + + if (Time.realtimeSinceStartup - lastSelectedSend > 0.2f) { - if (Multiplayer.Client == null || !Multiplayer.settings.showCursors || TickPatch.Simulating) return; - - var curMap = Find.CurrentMap.Index; - - foreach (var player in Multiplayer.session.players) - { - if (player.username == Multiplayer.username) continue; - if (player.map != curMap) continue; - if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; - - if (Multiplayer.settings.transparentPlayerCursors) - GUI.color = player.color * new Color(1, 1, 1, 0.5f); - else - GUI.color = player.color * new Color(1, 1, 1, 1); - - var pos = Vector3.Lerp( - player.lastCursor, - player.cursor, - (float)(Multiplayer.clock.ElapsedMillisDouble() - player.updatedAt) / 50f - ).MapToUIPosition(); - - var icon = MultiplayerData.icons.ElementAtOrDefault(player.cursorIcon); - var drawIcon = icon ?? CustomCursor.CursorTex; - var iconRect = new Rect(pos, new Vector2(24f * drawIcon.width / drawIcon.height, 24f)); - - Text.Font = GameFont.Tiny; - Text.Anchor = TextAnchor.MiddleCenter; - Widgets.Label(new Rect(pos, new Vector2(100, 30)).CenterOn(iconRect).Down(20f).Left(icon != null ? 0f : 5f), player.username); - Text.Anchor = TextAnchor.UpperLeft; - Text.Font = GameFont.Small; - - if (icon != null && MultiplayerData.iconInfos[player.cursorIcon].hasStuff) - GUI.color = new Color(0.5f, 0.4f, 0.26f, 0.5f); // Stuff color for wood - - GUI.DrawTexture(iconRect, drawIcon); - - if (player.dragStart != PlayerInfo.Invalid) - { - GUI.color = player.color * new Color(1, 1, 1, 0.2f); - Widgets.DrawBox(new Rect() { min = player.dragStart.MapToUIPosition(), max = pos }, 2); - } - - GUI.color = Color.white; - } + lastSelectedSend = Time.realtimeSinceStartup; + SendSelected(); } } - [HarmonyPatch(typeof(SelectionDrawer), nameof(SelectionDrawer.DrawSelectionOverlays))] - [StaticConstructorOnStartup] - static class SelectionBoxPatch + private void SendCursor() { - static Material graySelection = MaterialPool.MatFrom("UI/Overlays/SelectionBracket", ShaderDatabase.MetaOverlay); - static MaterialPropertyBlock propBlock = new MaterialPropertyBlock(); - static HashSet drawnThisUpdate = new HashSet(); - static Dictionary selTimes = new Dictionary(); + var writer = new ByteWriter(); + writer.WriteByte(cursorSeq++); + + if (Find.CurrentMap != null && !WorldRendererUtility.WorldRenderedNow) + { + writer.WriteByte((byte)Find.CurrentMap.Index); + + var icon = Find.MapUI?.designatorManager?.SelectedDesignator?.icon; + int iconId = icon == null ? 0 : + !MultiplayerData.icons.Contains(icon) ? 0 : MultiplayerData.icons.IndexOf(icon); + writer.WriteByte((byte)iconId); + + writer.WriteVectorXZ(UI.MouseMapPosition()); - static void Postfix() + if (Find.Selector.dragBox.IsValidAndActive) + writer.WriteVectorXZ(Find.Selector.dragBox.start); + else + writer.WriteShort(-1); + } + else { - if (Multiplayer.Client == null || TickPatch.Simulating) return; - - foreach (var t in Find.Selector.SelectedObjects.OfType()) - drawnThisUpdate.Add(t.thingIDNumber); - - foreach (var player in Multiplayer.session.players) - { - if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; - - foreach (var sel in player.selectedThings) - { - if (!drawnThisUpdate.Add(sel.Key)) continue; - if (!ThingsById.thingsById.TryGetValue(sel.Key, out Thing thing)) continue; - if (thing.Map != Find.CurrentMap) continue; - - selTimes[thing] = sel.Value; - SelectionDrawerUtility.CalculateSelectionBracketPositionsWorld(SelectionDrawer.bracketLocs, thing, thing.DrawPos, thing.RotatedSize.ToVector2(), selTimes, Vector2.one, 1f); - selTimes.Clear(); - - for (int i = 0; i < 4; i++) - { - Quaternion rotation = Quaternion.AngleAxis(-i * 90, Vector3.up); - propBlock.SetColor("_Color", player.color * new Color(1, 1, 1, 0.5f)); - Graphics.DrawMesh(MeshPool.plane10, SelectionDrawer.bracketLocs[i], rotation, graySelection, 0, null, 0, propBlock); - } - } - } - - drawnThisUpdate.Clear(); + writer.WriteByte(byte.MaxValue); } + + Multiplayer.Client.Send(Packets.Client_Cursor, writer.ToArray(), reliable: false); } - [HarmonyPatch(typeof(InspectPaneFiller), nameof(InspectPaneFiller.DrawInspectStringFor))] - static class DrawInspectPaneStringMarker + private void SendSelected() { - public static ISelectable drawingFor; + if (Current.ProgramState != ProgramState.Playing) return; - static void Prefix(ISelectable sel) => drawingFor = sel; - static void Postfix() => drawingFor = null; - } + var writer = new ByteWriter(); - [HarmonyPatch(typeof(InspectPaneFiller), nameof(InspectPaneFiller.DrawInspectString))] - static class DrawInspectStringPatch - { - static void Prefix(ref string str) + int mapId = Find.CurrentMap?.Index ?? -1; + if (WorldRendererUtility.WorldRenderedNow) mapId = -1; + + bool reset = false; + + if (mapId != lastMap) { - if (Multiplayer.Client == null) return; - if (!(DrawInspectPaneStringMarker.drawingFor is Thing thing)) return; + reset = true; + lastMap = mapId; + lastSelected.Clear(); + } - List players = new List(); + var selected = new HashSet(Find.Selector.selected.OfType().Select(t => t.thingIDNumber)); - foreach (var player in Multiplayer.session.players) - if (player.selectedThings.ContainsKey(thing.thingIDNumber)) - players.Add(player.username); + var add = new List(selected.Except(lastSelected)); + var remove = new List(lastSelected.Except(selected)); - if (players.Count > 0) - str += $"\nSelected by: {players.Join()}"; - } - } + if (!reset && add.Count == 0 && remove.Count == 0) return; + writer.WriteBool(reset); + writer.WritePrefixedInts(add); + writer.WritePrefixedInts(remove); + + lastSelected = selected; + + Multiplayer.Client.Send(Packets.Client_Selected, writer.ToArray()); + } } diff --git a/Source/Client/UI/ReplayTimeline.cs b/Source/Client/UI/ReplayTimeline.cs index 1b9ad42d..77f8f497 100644 --- a/Source/Client/UI/ReplayTimeline.cs +++ b/Source/Client/UI/ReplayTimeline.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using HarmonyLib; using Multiplayer.Client.Saving; using Multiplayer.Client.Util; -using Multiplayer.Common; using RimWorld; using RimWorld.Planet; using UnityEngine; @@ -52,24 +50,6 @@ private static void DrawTimelineWindow() MpUI.DrawRotatedLine(new Vector2((int)progressX, rect.center.y), TimelineHeight, 20f, 90f, Color.green); float mouseX = Event.current.mousePosition.x; - ReplayEvent mouseEvent = null; - - foreach (var ev in Multiplayer.session.events) - { - if (ev.time < timerStart || ev.time > timerEnd) - continue; - - var pointX = rect.xMin + (ev.time - timerStart) / (float)timeLen * rect.width; - - //GUI.DrawTexture(new Rect(pointX - 12f, rect.yMin - 24f, 24f, 24f), texture); - MpUI.DrawRotatedLine(new Vector2(pointX, rect.center.y), TimelineHeight, 20f, 90f, /*ev.color*/ Color.red); - - if (Mouse.IsOver(rect) && Math.Abs(mouseX - pointX) < 10) - { - mouseX = pointX; - mouseEvent = ev; - } - } // Draw mouse pointer and tooltip if (Mouse.IsOver(rect)) @@ -86,11 +66,7 @@ private static void DrawTimelineWindow() Event.current.Use(); string tooltip = $"Tick {mouseTimer}"; - if (mouseEvent != null) - tooltip = $"{mouseEvent.name}\n{tooltip}"; - const int tickTipId = 215462143; - TooltipHandler.TipRegion(rect, new TipSignal(tooltip, tickTipId)); // Remove delay between the mouseover and showing @@ -109,7 +85,7 @@ private static void DrawTimelineWindow() private static void SimulateToTick(int targetTick) { - TickPatch.SetSimulation(targetTick, canESC: true); + TickPatch.SetSimulation(targetTick, canEsc: true); if (targetTick < TickPatch.Timer) { diff --git a/Source/Client/Util/Extensions.cs b/Source/Client/Util/Extensions.cs index e563a73a..252ed092 100644 --- a/Source/Client/Util/Extensions.cs +++ b/Source/Client/Util/Extensions.cs @@ -23,46 +23,6 @@ public static class Extensions { private static Regex methodNameCleaner = new Regex(@"(\?[0-9\-]+)"); - // Sets the current Faction.OfPlayer - // Applies faction's world components - // Applies faction's map components if map not null - public static void PushFaction(this Map map, Faction f) - { - var faction = FactionContext.Push(f); - if (faction == null) return; - - Multiplayer.WorldComp?.SetFaction(faction); - map?.MpComp().SetFaction(faction); - } - - public static void PushFaction(this Map map, int factionId) - { - Faction faction = Find.FactionManager.GetById(factionId); - map.PushFaction(faction); - } - - public static Faction PopFaction() - { - return PopFaction(null); - } - - public static Faction PopFaction(this Container? c) - { - if (!c.HasValue) return null; - return PopFaction(c.Value.Inner); - } - - public static Faction PopFaction(this Map map) - { - Faction faction = FactionContext.Pop(); - if (faction == null) return null; - - Multiplayer.WorldComp?.SetFaction(faction); - map?.MpComp().SetFaction(faction); - - return faction; - } - public static Map GetMap(this ScheduledCommand cmd) { if (cmd.mapId == ScheduledCommand.Global) return null; diff --git a/Source/Client/Util/MethodOf.cs b/Source/Client/Util/MethodOf.cs new file mode 100644 index 00000000..5b921e05 --- /dev/null +++ b/Source/Client/Util/MethodOf.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using HarmonyLib; + +namespace Multiplayer.Client.Util; + +public static class MethodOf +{ + /// Given a lambda expression that calls a method, returns the method info + /// The lambda expression using the method + /// The method in the lambda expression + /// + public static MethodInfo Inner(Expression expression) + { + return Inner((LambdaExpression)expression); + } + + /// Given a lambda expression that calls a method, returns the method info + /// The generic type + /// The lambda expression using the method + /// The method in the lambda expression + /// + public static MethodInfo Inner(Expression> expression) + { + return Inner((LambdaExpression)expression); + } + + /// Given a lambda expression that calls a method, returns the method info + /// The generic type + /// The generic result type + /// The lambda expression using the method + /// The method in the lambda expression + /// + public static MethodInfo Inner(Expression> expression) + { + return Inner((LambdaExpression)expression); + } + + /// Given a lambda expression that calls a method, returns the method info + /// The lambda expression using the method + /// The method in the lambda expression + /// + public static MethodInfo Inner(LambdaExpression expression) + { + if (expression.Body is not MethodCallExpression outermostExpression) + { + if (expression.Body is UnaryExpression ue && ue.Operand is MethodCallExpression me && + me.Object is ConstantExpression ce && ce.Value is MethodInfo mi) + return mi; + throw new ArgumentException("Invalid Expression. Expression should consist of a Method call only."); + } + + var method = outermostExpression.Method; + if (method is null) + throw new Exception($"Cannot find method for expression {expression}"); + + return method; + } + + public static MethodInfo Lambda(Delegate del) + { + return del.Method; + } + + public static HarmonyMethod Harmony(this MethodInfo m) + { + return new HarmonyMethod(m); + } +} diff --git a/Source/Client/Util/MpUI.cs b/Source/Client/Util/MpUI.cs index bae16740..23244053 100644 --- a/Source/Client/Util/MpUI.cs +++ b/Source/Client/Util/MpUI.cs @@ -7,7 +7,6 @@ namespace Multiplayer.Client.Util { - public static class MpUI { public static Vector2 Resolution => new Vector2(UI.screenWidth, UI.screenHeight); diff --git a/Source/Client/Util/MpUtil.cs b/Source/Client/Util/MpUtil.cs index a65d2085..fbf58145 100644 --- a/Source/Client/Util/MpUtil.cs +++ b/Source/Client/Util/MpUtil.cs @@ -11,7 +11,6 @@ using System.Runtime.Serialization; using System.Text; using HarmonyLib; -using MonoMod.RuntimeDetour; using Verse; namespace Multiplayer.Client @@ -109,7 +108,7 @@ public static MethodBase GetOriginalFromHarmonyReplacement(long replacementAddr) // Todo: this is using a non-public API of Harmony, we should refactor to use https://harmony.pardeike.net/api/HarmonyLib.Harmony.html#HarmonyLib_Harmony_GetOriginalMethod_System_Reflection_MethodInfo_ // as pardeike suggested in https://github.com/rwmt/Multiplayer/pull/270#issuecomment-1003298289 return HarmonySharedState.originals - .FirstOrDefault(kv => kv.Key.GetNativeStart().ToInt64() == replacementAddr).Value; + .FirstOrDefault(kv => kv.Key.MethodHandle.GetFunctionPointer().ToInt64() == replacementAddr).Value; } public static string JoinStringsAtMost(this IEnumerable strs, int atMost = 3) diff --git a/Source/Client/Windows/DesyncedWindow.cs b/Source/Client/Windows/DesyncedWindow.cs index e5b77428..a92d8f83 100644 --- a/Source/Client/Windows/DesyncedWindow.cs +++ b/Source/Client/Windows/DesyncedWindow.cs @@ -1,13 +1,10 @@ -using Multiplayer.Common; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; -using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { - [HotSwappable] public class DesyncedWindow : Window { const int NumButtons = 5; diff --git a/Source/Client/Windows/HostWindow.cs b/Source/Client/Windows/HostWindow.cs index f0c67168..baf10c23 100644 --- a/Source/Client/Windows/HostWindow.cs +++ b/Source/Client/Windows/HostWindow.cs @@ -21,35 +21,38 @@ enum Tab Connecting, Gameplay } - public override Vector2 InitialSize => new(550f, 430f); + public override Vector2 InitialSize => new(550f, 460f); private SaveFile file; public bool returnToServerBrowser; - private bool hadSimulation; - private bool asyncTime; - private bool asyncTimeLocked; private Tab tab; + private bool asyncTimeLocked; + private bool multifactionLocked; + private float height; private ServerSettings serverSettings; - public HostWindow(SaveFile file = null, bool hadSimulation = false) + public HostWindow(SaveFile file = null) { closeOnAccept = false; doCloseX = true; - serverSettings = Multiplayer.settings.ServerSettings; + serverSettings = Multiplayer.settings.PreferredLocalServerSettings; - this.hadSimulation = hadSimulation; this.file = file; serverSettings.gameName = file?.gameName ?? Multiplayer.session?.gameName ?? $"{Multiplayer.username}'s game"; - asyncTime = file?.asyncTime ?? Multiplayer.game?.gameComp.asyncTime ?? false; + serverSettings.asyncTime = file?.asyncTime ?? Multiplayer.game?.gameComp.asyncTime ?? false; + serverSettings.multifaction = file?.multifaction ?? Multiplayer.game?.gameComp.multifaction ?? false; - if (asyncTime) + if (serverSettings.asyncTime) asyncTimeLocked = true; // Once enabled in a save, cannot be disabled + if (serverSettings.multifaction) + multifactionLocked = true; + var localAddr = MpUtil.GetLocalIpAddress() ?? "127.0.0.1"; serverSettings.lanAddress = localAddr; @@ -217,9 +220,13 @@ private void DoGameplay(Rect entry) entry = entry.Down(30); + // Multifaction + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"Multifaction: ", ref serverSettings.multifaction, order: ElementOrder.Right, disabled: multifactionLocked); + entry = entry.Down(30); + // Async time TooltipHandler.TipRegion(entry.Width(CheckboxWidth), $"{"MpAsyncTimeDesc".Translate()}\n\n{"MpExperimentalFeature".Translate()}"); - MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpAsyncTime".Translate()}: ", ref asyncTime, order: ElementOrder.Right, disabled: asyncTimeLocked); + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpAsyncTime".Translate()}: ", ref serverSettings.asyncTime, order: ElementOrder.Right, disabled: asyncTimeLocked); entry = entry.Down(30); // Time control @@ -495,19 +502,19 @@ private void HostFromSpSaveFile(ServerSettings settings) LongEventHandler.ExecuteWhenFinished(() => { - LongEventHandler.QueueLongEvent(() => HostUtil.HostServer(settings, false, false, asyncTime), "MpLoading", false, null); + LongEventHandler.QueueLongEvent(() => HostUtil.HostServer(settings, false), "MpLoading", false, null); }); }, "Play", "LoadingLongEvent", true, null); } private void HostFromSpIngame(ServerSettings settings) { - HostUtil.HostServer(settings, false, false, asyncTime); + HostUtil.HostServer(settings, false); } private void HostFromReplay(ServerSettings settings) { - void ReplayLoaded() => HostUtil.HostServer(settings, true, hadSimulation, asyncTime); + void ReplayLoaded() => HostUtil.HostServer(settings, true); if (file != null) { diff --git a/Source/Client/Windows/JoinDataWindow.cs b/Source/Client/Windows/JoinDataWindow.cs index 70b1f40f..ffd2aff2 100644 --- a/Source/Client/Windows/JoinDataWindow.cs +++ b/Source/Client/Windows/JoinDataWindow.cs @@ -168,7 +168,7 @@ void AddConfigs(List one, List two, NodeStatus notInTwo) } } - var localConfigs = JoinData.GetSyncableConfigContents(remote.RemoteModIds); + var localConfigs = JoinData.GetSyncableConfigContents(remote.RemoteModIds.ToList()); if (remote.hasConfigs) { @@ -676,7 +676,6 @@ void DrawModListItem(Vector2 topLeft, string name, string tip, ContentSource sou } } - public class FixAndRestartWindow : Window { private RemoteData data; @@ -732,17 +731,19 @@ private void DoRestart() { if (applyModList) { - ModsConfig.SetActiveToList(data.remoteMods.Select(m => + ModsConfig.data.activeMods = data.remoteMods.Select(m => { - var activeMod = ModsConfig.ActiveModsInLoadOrder.FirstOrDefault(a => a.PackageIdNonUnique == m.packageId); + var activeMod = + ModsConfig.ActiveModsInLoadOrder.FirstOrDefault(a => a.PackageIdNonUnique == m.packageId); if (activeMod != null) return activeMod.PackageId; if (!m.packageId.Contains("ludeon")) - return m.packageId + ModMetaData.SteamModPostfix; // Prefer the Steam version for new mods on the list + return m.packageId + + ModMetaData.SteamModPostfix; // Prefer the Steam version for new mods on the list return m.packageId; - }).ToList()); + }).ToList(); ModsConfig.Save(); } diff --git a/Source/Client/Windows/PacketLogWindow.cs b/Source/Client/Windows/PacketLogWindow.cs index d591ccf0..6b00355a 100644 --- a/Source/Client/Windows/PacketLogWindow.cs +++ b/Source/Client/Windows/PacketLogWindow.cs @@ -7,17 +7,20 @@ namespace Multiplayer.Client public class PacketLogWindow : Window { - public override Vector2 InitialSize => new Vector2(UI.screenWidth / 2f, UI.screenHeight / 2f); + public override Vector2 InitialSize => new(UI.screenWidth / 2f, UI.screenHeight / 2f); - private List nodes = new List(); + private List nodes = new(); private int logHeight; private Vector2 scrollPos; + private bool storeStackTrace; public int NodeCount => nodes.Count; - public PacketLogWindow() + public PacketLogWindow(bool storeStackTrace) { doCloseX = true; + + this.storeStackTrace = storeStackTrace; } public override void DoWindowContents(Rect rect) @@ -72,6 +75,15 @@ public void Draw(LogNode node, int depth, ref Rect rect) public void AddCurrentNode(IHasLogger hasLogger) { + if (storeStackTrace) + hasLogger.Log.current.children.Add(new LogNode("Stack trace") + { + children = + { + new LogNode(StackTraceUtility.ExtractStackTrace()) + } + }); + nodes.Add(hasLogger.Log.current); } } diff --git a/Source/Client/Windows/SaveFileReader.cs b/Source/Client/Windows/SaveFileReader.cs index cfead819..8f8fe909 100644 --- a/Source/Client/Windows/SaveFileReader.cs +++ b/Source/Client/Windows/SaveFileReader.cs @@ -76,6 +76,7 @@ private void ReadMpSave(FileInfo file) saveFile.modIds = replay.info.modIds.ToArray(); saveFile.modNames = replay.info.modNames.ToArray(); saveFile.asyncTime = replay.info.asyncTime; + saveFile.multifaction = replay.info.multifaction; } else { @@ -138,6 +139,7 @@ public class SaveFile public int protocol; public bool asyncTime; + public bool multifaction; public bool HasRwVersion => rwVersion != null; diff --git a/Source/Client/Windows/SaveGameWindow.cs b/Source/Client/Windows/SaveGameWindow.cs index 38458c1a..ec22ccbc 100644 --- a/Source/Client/Windows/SaveGameWindow.cs +++ b/Source/Client/Windows/SaveGameWindow.cs @@ -1,17 +1,15 @@ using System.IO; using Multiplayer.Client.Util; -using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client; -[HotSwappable] public class SaveGameWindow : Window { public override Vector2 InitialSize => new(350f, 175f); - private string curText; + private string curText = ""; private bool fileExists; public SaveGameWindow(string gameName) @@ -20,7 +18,7 @@ public SaveGameWindow(string gameName) doCloseX = true; absorbInputAroundWindow = true; closeOnAccept = true; - curText = GenFile.SanitizedFileName(gameName); + UpdateText(ref curText, GenFile.SanitizedFileName(gameName)); } public override void DoWindowContents(Rect inRect) @@ -73,7 +71,7 @@ private void Accept(bool currentReplay) { if (curText.Length != 0) { - LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile(curText, currentReplay), "MpSaving", false, null); + LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile_Overwrite(curText, currentReplay), "MpSaving", false, null); Close(); } } diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index f67a36f8..bbe35248 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -369,17 +369,19 @@ private void DrawSaveList(List saves, float width, ref float y) var text = "MpSaveOutdatedDesc".Translate(data.rwVersion, VersionControl.CurrentVersionString); TooltipHandler.TipRegion(outdated, text); } + } - Text.Font = GameFont.Small; - GUI.color = Color.white; + Text.Font = GameFont.Small; + GUI.color = Color.white; - if (Widgets.ButtonInvisible(entryRect, false)) - { - if (Event.current.button == 0) - selectedFile = file; - else if (Event.current.button == 1 && data.HasRwVersion) - Find.WindowStack.Add(new FloatMenu(SaveFloatMenu(data).ToList())); - } + // Check data != null after drawing the button so draw order doesn't depend on data + // and IMGUI control ids are the same across frames + if (Widgets.ButtonInvisible(entryRect, false) && data != null) + { + if (Event.current.button == 0) + selectedFile = file; + else if (Event.current.button == 1 && data.HasRwVersion) + Find.WindowStack.Add(new FloatMenu(SaveFloatMenu(data).ToList())); } y += 40; diff --git a/Source/Common/ChatCommands.cs b/Source/Common/ChatCommands.cs index bb14dae4..09136f2c 100644 --- a/Source/Common/ChatCommands.cs +++ b/Source/Common/ChatCommands.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace Multiplayer.Common +namespace Multiplayer.Common { public abstract class ChatCmdHandler { @@ -15,7 +13,7 @@ public void SendNoPermission(ServerPlayer player) player.SendChat("You don't have permission."); } - public ServerPlayer FindPlayer(string username) + public ServerPlayer? FindPlayer(string username) { return Server.GetPlayer(username); } diff --git a/Source/Common/CommandHandler.cs b/Source/Common/CommandHandler.cs index 022419d1..1aa43286 100644 --- a/Source/Common/CommandHandler.cs +++ b/Source/Common/CommandHandler.cs @@ -18,6 +18,7 @@ public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerP if (server.freezeManager.Frozen) return; + // policy if (sourcePlayer != null) { bool debugCmd = diff --git a/Source/Common/CommandType.cs b/Source/Common/CommandType.cs new file mode 100644 index 00000000..ba3ce05d --- /dev/null +++ b/Source/Common/CommandType.cs @@ -0,0 +1,19 @@ +namespace Multiplayer.Common; + +public enum CommandType : byte +{ + // Global scope + GlobalTimeSpeed, + TimeSpeedVote, + PauseAll, + CreateJoinPoint, + InitPlayerData, + + // Mixed scope + Sync, + DebugTools, + + // Map scope + MapTimeSpeed, + Designator, +} diff --git a/Source/Common/Commands.cs b/Source/Common/Commands.cs deleted file mode 100644 index e4bcc579..00000000 --- a/Source/Common/Commands.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Multiplayer.Common -{ - public enum CommandType : byte - { - // Global scope - GlobalTimeSpeed, - TimeSpeedVote, - PauseAll, - CreateJoinPoint, - SetupFaction, - GlobalIdBlock, - InitPlayerData, - - // Mixed scope - Sync, - DebugTools, - - // Map scope - MapTimeSpeed, - CreateMapFactionData, - MapIdBlock, - Designator, - } -} diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index f035ffb7..45f2ee52 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -4,7 +4,7 @@ net472 true enable - 10 + 11 false false Multiplayer.Common diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs new file mode 100644 index 00000000..8e5cdd43 --- /dev/null +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -0,0 +1,262 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Multiplayer.Client.Desyncs; + +public static class DeferredStackTracingImpl +{ + struct AddrInfo + { + public long addr; + public long stackUsage; + public long nameHash; + public long unused; + } + + const int StartingN = 7; + const int StartingShift = 64 - StartingN; + const int StartingSize = 1 << StartingN; + const float LoadFactor = 0.5f; + + static AddrInfo[] hashtable = new AddrInfo[StartingSize]; + public static int hashtableSize = StartingSize; + public static int hashtableEntries; + public static int hashtableShift = StartingShift; + public static int collisions; + + const long NotJIT = long.MaxValue; + const long RBPBased = long.MaxValue - 1; + + const long UsesRBPAsGPR = 1 << 50; + const long UsesRBX = 1 << 51; + const long RBPInfoClearMask = ~(UsesRBPAsGPR | UsesRBX); + + public const int MaxDepth = 32; + public const int HashInfluence = 6; + + public unsafe static int TraceImpl(long[] traceIn, ref int hash) + { + long[] trace = traceIn; + long rbp = GetRbp(); + long stck = rbp; + rbp = *(long*)rbp; + + int indexmask = hashtableSize - 1; + int shift = hashtableShift; + + long ret; + long lmfPtr = *(long*)Native.LmfPtr; + + int depth = 0; + + while (true) + { + ret = *(long*)(stck + 8); + + int index = (int)(HashAddr((ulong)ret) >> shift); + ref var info = ref hashtable[index]; + int colls = 0; + + // Open addressing + while (info.addr != 0 && info.addr != ret) + { + index = (index + 1) & indexmask; + info = ref hashtable[index]; + colls++; + } + + if (colls > collisions) + collisions = colls; + + long stackUsage = 0; + + if (info.addr != 0) + stackUsage = info.stackUsage; + else + stackUsage = UpdateNewElement(ref info, ret); + + if (stackUsage == NotJIT) + { + // LMF (Last Managed Frame) layout on x64: + // previous + // rbp + // rsp + + lmfPtr = *(long*)lmfPtr; + var lmfRbp = *(long*)(lmfPtr + 8); + + if (lmfPtr == 0 || lmfRbp == 0) + break; + + rbp = lmfRbp; + stck = *(long*)(lmfPtr + 16) - 16; + + continue; + } + + trace[depth] = ret; + + if (depth < HashInfluence) + hash = HashCombineInt(hash, (int)info.nameHash); + + if (++depth == MaxDepth) + break; + + if (stackUsage == RBPBased) + { + stck = rbp; + rbp = *(long*)rbp; + continue; + } + + stck += 8; + + if ((stackUsage & UsesRBPAsGPR) != 0) + { + if ((stackUsage & UsesRBX) != 0) + rbp = *(long*)(stck + 16); + else + rbp = *(long*)(stck + 8); + + stackUsage &= RBPInfoClearMask; + } + + stck += stackUsage; + } + + return depth; + } + + static long UpdateNewElement(ref AddrInfo info, long ret) + { + long stackUsage = GetStackUsage(ret); + + info.addr = ret; + info.stackUsage = stackUsage; + + var rawName = Native.MethodNameFromAddr(ret, true); // Use the original instead of replacement for hashing + info.nameHash = rawName != null ? StableStringHash(rawName) : 1; + + hashtableEntries++; + if (hashtableEntries > hashtableSize * LoadFactor) + ResizeHashtable(); + + return stackUsage; + } + + static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485; + + static int ResizeHashtable() + { + var oldTable = hashtable; + + hashtableSize *= 2; + hashtableShift--; + + hashtable = new AddrInfo[hashtableSize]; + collisions = 0; + + int indexmask = hashtableSize - 1; + int shift = hashtableShift; + + for (int i = 0; i < oldTable.Length; i++) + { + ref var oldInfo = ref oldTable[i]; + if (oldInfo.addr != 0) + { + int index = (int)(HashAddr((ulong)oldInfo.addr) >> shift); + + while (hashtable[index].addr != 0) + index = (index + 1) & indexmask; + + ref var newInfo = ref hashtable[index]; + newInfo.addr = oldInfo.addr; + newInfo.stackUsage = oldInfo.stackUsage; + newInfo.nameHash = oldInfo.nameHash; + } + } + + return indexmask; + } + + unsafe static long GetStackUsage(long addr) + { + var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); + + if (ji == IntPtr.Zero) + return NotJIT; + + var start = (uint*)Native.mono_jit_info_get_code_start(ji); + long usage = 0; + + if ((*start & 0xFFFFFF) == 0xEC8348) // sub rsp,XX (4883EC XX) + { + usage = *start >> 24; + start += 1; + } else if ((*start & 0xFFFFFF) == 0xEC8148) // sub rsp,XXXXXXXX (4881EC XXXXXXXX) + { + usage = *(uint*)((long)start + 3); + start = (uint*)((long)start + 7); + } + + if (usage != 0) + { + CheckRbpUsage(start, ref usage); + return usage; + } + + // push rbp (55) + if (*(byte*)start == 0x55) + return RBPBased; + + throw new Exception($"Deferred stack tracing: Unknown function header {*start} {Native.MethodNameFromAddr(addr, false)}"); + } + + private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage) + { + // If rbp is used as a gp reg then the prologue looks like (after frame alloc): + // mov [rsp],rbp (48892C24) + // or: + // mov [rsp],rbx (48891C24) + // mov [rsp+8],rbp (48896C2408) + // (The calle saved registers are always in the same order + // and are saved at the bottom of the frame) + + if (*at == 0x242C8948) + { + stackUsage |= UsesRBPAsGPR; + } + else if (*at == 0x241C8948 && *(at + 1) == 0x246C8948) + { + stackUsage |= UsesRBPAsGPR; + stackUsage |= UsesRBX; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static unsafe long GetRbp() + { + long rbp = 0; + return *(&rbp + 1); + } + + public static int HashCombineInt(int seed, int value) + { + return (int)(seed ^ (value + 2654435769u + (seed << 6) + (seed >> 2))); + } + + public static int StableStringHash(string str) + { + if (str == null) + { + return 0; + } + int num = 23; + int length = str.Length; + for (int i = 0; i < length; i++) + { + num = num * 31 + str[i]; + } + return num; + } +} diff --git a/Source/Common/IdBlock.cs b/Source/Common/IdBlock.cs deleted file mode 100644 index 4e43f635..00000000 --- a/Source/Common/IdBlock.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Multiplayer.Common -{ - public class IdBlock - { - public int blockStart; - public int blockSize; - public int mapId; - - public int currentWithinBlock; - public bool overflowHandled; - - public int Current => blockStart + currentWithinBlock; - - public IdBlock(int blockStart, int blockSize, int mapId = -1) - { - this.blockStart = blockStart; - this.blockSize = blockSize; - this.mapId = mapId; - } - - public int NextId() - { - // Overflows should be handled by the caller - currentWithinBlock++; - return blockStart + currentWithinBlock; - } - - public byte[] Serialize() - { - ByteWriter writer = new ByteWriter(); - writer.WriteInt32(blockStart); - writer.WriteInt32(blockSize); - writer.WriteInt32(mapId); - writer.WriteInt32(currentWithinBlock); - - return writer.ToArray(); - } - - public static IdBlock Deserialize(ByteReader data) - { - IdBlock block = new IdBlock(data.ReadInt32(), data.ReadInt32(), data.ReadInt32()); - block.currentWithinBlock = data.ReadInt32(); - return block; - } - } -} diff --git a/Source/Common/LiteNetManager.cs b/Source/Common/LiteNetManager.cs index f0bfbb79..f6f571b5 100644 --- a/Source/Common/LiteNetManager.cs +++ b/Source/Common/LiteNetManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; -using System.Text; using LiteNetLib; using Multiplayer.Common.Util; @@ -34,7 +33,7 @@ public void Tick() arbiter?.PollEvents(); if (lanManager != null && broadcastTimer % 60 == 0) - lanManager.SendBroadcast(Encoding.UTF8.GetBytes("mp-server"), 5100); + lanManager.SendBroadcast("mp-server"u8.ToArray(), 5100); broadcastTimer++; } diff --git a/Source/Common/MultiplayerServer.cs b/Source/Common/MultiplayerServer.cs index 0c2af38e..b18ff5da 100644 --- a/Source/Common/MultiplayerServer.cs +++ b/Source/Common/MultiplayerServer.cs @@ -38,6 +38,7 @@ static MultiplayerServer() public string? hostUsername; public int gameTimer; + public int startingTimer; public int workTicks; public ActionQueue queue = new(); public ServerSettings settings; @@ -48,8 +49,6 @@ static MultiplayerServer() private Dictionary chatCmdHandlers = new(); - public int nextUniqueId; // currently unused - public volatile bool running; public event Action? TickEvent; @@ -214,15 +213,6 @@ public void SendToPlaying(Packets id, byte[] data, bool reliable = true, ServerP return playerManager.GetPlayer(id); } - public IdBlock NextIdBlock(int blockSize = 30000) - { - int blockStart = nextUniqueId; - nextUniqueId += blockSize; - ServerLog.Log($"New id block {blockStart} of size {blockSize}"); - - return new IdBlock(blockStart, blockSize); - } - public void SendChat(string msg) { ServerLog.Detail($"[Chat] {msg}"); diff --git a/Source/Client/Native.cs b/Source/Common/Native.cs similarity index 83% rename from Source/Client/Native.cs rename to Source/Common/Native.cs index cc80f6ab..290aad7a 100644 --- a/Source/Client/Native.cs +++ b/Source/Common/Native.cs @@ -1,32 +1,29 @@ using System; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; -using HarmonyLib; -//using Iced.Intel; -using UnityEngine; -using Verse; -//using Decoder = Iced.Intel.Decoder; namespace Multiplayer.Client { - static class Native + public static class Native { + public enum NativeOS + { + Windows, OSX, Linux, Dummy + } + public static IntPtr DomainPtr { get; private set; } // LMF is Last Managed Frame // current_thread->internal_thread->thread_info->tls[4] public static long LmfPtr { get; private set; } - public static bool Linux => Application.platform == RuntimePlatform.LinuxEditor || Application.platform == RuntimePlatform.LinuxPlayer; - public static bool Windows => Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer; - public static bool OSX => Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer; - - public static void EarlyInit() + public static void EarlyInit(NativeOS os) { - if (Linux) + if (os == NativeOS.Linux) TheLinuxWay(); - if (OSX) + if (os == NativeOS.OSX) TheOSXWay(); EarlyInitInternal(); @@ -44,27 +41,33 @@ private static void EarlyInitInternal() DomainPtr = mono_domain_get(); } - public static void InitLmfPtr() + public static unsafe void InitLmfPtr(NativeOS os) { - if (!UnityData.IsInMainThread) - throw new Exception("Multiplayer.Client.Native data getter not running on the main thread!"); - // Don't bother on 32 bit runtimes if (IntPtr.Size == 4) return; - var internalThreadField = AccessTools.Field(typeof(Thread), "internal_thread"); - var threadInfoField = AccessTools.Field(internalThreadField.FieldType, "runtime_thread_info"); + const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic; + + var internalThreadField = typeof(Thread).GetField("internal_thread", all); + var threadInfoField = internalThreadField.FieldType.GetField("runtime_thread_info", all); var threadInfoPtr = (long)(IntPtr)threadInfoField.GetValue(internalThreadField.GetValue(Thread.CurrentThread)); // Struct offset found manually // Navigate by string: "Handle Stack" - if (Linux) + if (os == NativeOS.Linux) LmfPtr = threadInfoPtr + 0x480 - 8 * 4; - else if (Windows) + else if (os == NativeOS.Windows) LmfPtr = threadInfoPtr + 0x448 - 8 * 4; - else if (OSX) + else if (os == NativeOS.OSX) LmfPtr = threadInfoPtr + 0x418 - 8 * 4; + else if (os == NativeOS.Dummy) + { + LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); + *(long*)LmfPtr = LmfPtr; + *(long*)(LmfPtr + 8) = 0; + } } public static string MethodNameFromAddr(long addr, bool harmonyOriginals) @@ -79,9 +82,9 @@ public static string MethodNameFromAddr(long addr, bool harmonyOriginals) if (harmonyOriginals) { - var original = MpUtil.GetOriginalFromHarmonyReplacement(codeStart); - if (original != null) - ptrToPrint = original.MethodHandle.Value; + // var original = MpUtil.GetOriginalFromHarmonyReplacement(codeStart); + // if (original != null) + // ptrToPrint = original.MethodHandle.Value; } var name = mono_debug_print_stack_frame(ptrToPrint, -1, domain); @@ -117,6 +120,9 @@ public static string MethodNameFromAddr(long addr, bool harmonyOriginals) [DllImport(MonoWindows)] public static extern int mini_parse_debug_option(string option); + [DllImport(MonoWindows)] + public static extern void mono_set_defaults(int verboseLevel, int opts); + [DllImport(MonoWindows)] public static extern IntPtr mono_domain_get(); diff --git a/Source/Common/Networking/ConnectionBase.cs b/Source/Common/Networking/ConnectionBase.cs index 4bf32832..8bb64cb7 100644 --- a/Source/Common/Networking/ConnectionBase.cs +++ b/Source/Common/Networking/ConnectionBase.cs @@ -30,12 +30,12 @@ public void ChangeState(ConnectionStateEnum state) StateObj?.StartState(); } - public virtual void Send(Packets id) + public void Send(Packets id) { Send(id, Array.Empty()); } - public virtual void Send(Packets id, params object[] msg) + public void Send(Packets id, params object[] msg) { Send(id, ByteWriter.GetBytes(msg)); } @@ -64,7 +64,7 @@ public virtual void Send(Packets id, byte[] message, bool reliable = true) private const int FragEnd = 0x80; // All fragmented packets need to be sent from the same thread - public virtual void SendFragmented(Packets id, byte[] message) + public void SendFragmented(Packets id, byte[] message) { if (State == ConnectionStateEnum.Disconnected) return; @@ -93,7 +93,7 @@ public virtual void SendFragmented(Packets id, byte[] message) } } - public virtual void SendFragmented(Packets id, params object[] msg) + public void SendFragmented(Packets id, params object[] msg) { SendFragmented(id, ByteWriter.GetBytes(msg)); } @@ -116,7 +116,7 @@ public virtual void HandleReceiveRaw(ByteReader data, bool reliable) } private ByteWriter? fragmented; - private int fullSize; // For information, doesn't affect anything + private int fullSize; // Only for UI public int FragmentProgress => (fragmented?.Position * 100 / fullSize) ?? 0; diff --git a/Source/Common/Networking/Packets.cs b/Source/Common/Networking/Packets.cs index 1e1f0431..b7deb0fe 100644 --- a/Source/Common/Networking/Packets.cs +++ b/Source/Common/Networking/Packets.cs @@ -19,7 +19,6 @@ public enum Packets : byte Client_WorldReady, Client_Command, Client_WorldDataUpload, - Client_IdBlockRequest, Client_Chat, Client_KeepAlive, Client_SteamRequest, diff --git a/Source/Common/Networking/State/ServerJoiningState.cs b/Source/Common/Networking/State/ServerJoiningState.cs index 982670ac..9dd315a9 100644 --- a/Source/Common/Networking/State/ServerJoiningState.cs +++ b/Source/Common/Networking/State/ServerJoiningState.cs @@ -13,7 +13,7 @@ protected override async Task RunState() HandleProtocol(await Packet(Packets.Client_Protocol)); HandleUsername(await Packet(Packets.Client_Username)); - while (await Server.InitData() is not { } && await EndIfDead()) + while (await Server.InitData() is null && await EndIfDead()) if (Server.initDataState == InitDataState.Waiting) await RequestInitData(); diff --git a/Source/Common/Networking/State/ServerLoadingState.cs b/Source/Common/Networking/State/ServerLoadingState.cs index 7b9507b9..95cb59c3 100644 --- a/Source/Common/Networking/State/ServerLoadingState.cs +++ b/Source/Common/Networking/State/ServerLoadingState.cs @@ -28,7 +28,7 @@ public void SendWorldData() writer.WriteInt32(Player.FactionId); writer.WriteInt32(Server.gameTimer); - writer.WriteInt32(Server.sentCmdsSnapshot); + writer.WriteInt32(Server.commands.SentCmds); writer.WriteBool(Server.freezeManager.Frozen); writer.WritePrefixedBytes(Server.worldData.savedGame); writer.WritePrefixedBytes(Server.worldData.semiPersistent); diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index 464b1af7..203d7ef6 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; using System.Linq; -using Verse; namespace Multiplayer.Common { @@ -51,7 +51,8 @@ public void HandleClientCommand(ByteReader data) { CommandType cmd = (CommandType)data.ReadInt32(); int mapId = data.ReadInt32(); - byte[] extra = data.ReadPrefixedBytes(65535); + byte[]? extra = data.ReadPrefixedBytes(65535); + if (extra == null) return; // todo check if map id is valid for the player @@ -84,7 +85,7 @@ public void HandleChat(ByteReader data) [IsFragmented] public void HandleWorldDataUpload(ByteReader data) { - if (Server.ArbiterPlaying ? !Player.IsArbiter : !Player.IsHost) + if (Server.ArbiterPlaying ? !Player.IsArbiter : !Player.IsHost) // policy return; ServerLog.Log($"Got world upload {data.Left}"); @@ -108,7 +109,7 @@ public void HandleWorldDataUpload(ByteReader data) [PacketHandler(Packets.Client_Cursor)] public void HandleCursor(ByteReader data) { - if (Player.lastCursorTick == Server.NetTimer) return; + if (Player.lastCursorTick == Server.NetTimer) return; // policy var writer = new ByteWriter(); @@ -175,22 +176,6 @@ public void HandlePing(ByteReader data) Server.SendToPlaying(Packets.Server_PingLocation, writer.ToArray()); } - [PacketHandler(Packets.Client_IdBlockRequest)] - public void HandleIdBlockRequest(ByteReader data) - { - int mapId = data.ReadInt32(); - - if (mapId == ScheduledCommand.Global) - { - //IdBlock nextBlock = MultiplayerServer.instance.NextIdBlock(); - //MultiplayerServer.instance.SendCommand(CommandType.GlobalIdBlock, ScheduledCommand.NoFaction, ScheduledCommand.Global, nextBlock.Serialize()); - } - else - { - // todo - } - } - [PacketHandler(Packets.Client_KeepAlive)] public void HandleClientKeepAlive(ByteReader data) { @@ -224,7 +209,7 @@ public void HandleClientKeepAlive(ByteReader data) public void HandleDesyncCheck(ByteReader data) { var arbiter = Server.ArbiterPlaying; - if (arbiter ? !Player.IsArbiter : !Player.IsHost) return; + if (arbiter ? !Player.IsArbiter : !Player.IsHost) return; // policy var raw = data.ReadRaw(data.Left); @@ -250,6 +235,7 @@ public void HandleFreeze(ByteReader data) [PacketHandler(Packets.Client_Autosaving)] public void HandleAutosaving(ByteReader data) { + // Host policy if (Player.IsHost && Server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Autosave)) Server.worldData.TryStartJoinPointCreation(); } @@ -257,12 +243,18 @@ public void HandleAutosaving(ByteReader data) [PacketHandler(Packets.Client_Debug)] public void HandleDebug(ByteReader data) { + // todo restrict handling + + Server.worldData.mapCmds.Clear(); + Server.gameTimer = Server.startingTimer; + + Server.SendToPlaying(Packets.Server_Debug, Array.Empty()); } [PacketHandler(Packets.Client_SetFaction)] public void HandleSetFaction(ByteReader data) { - if (!Player.IsHost) return; + // todo restrict handling int playerId = data.ReadInt32(); int factionId = data.ReadInt32(); diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index 68dcf3b3..67a9b502 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -131,7 +131,7 @@ public void OnDesync(ServerPlayer player, int tick, int diffAt) public void OnJoin(ServerPlayer player) { player.hasJoined = true; - player.FactionId = server.worldData.defaultFactionId; + player.FactionId = player.id == 0 ? server.worldData.hostFactionId : server.worldData.spectatorFactionId; server.SendNotification("MpPlayerConnected", player.Username); server.SendChat($"{player.Username} has joined."); diff --git a/Source/Common/ReplayInfo.cs b/Source/Common/ReplayInfo.cs index 7e67189a..864c66dd 100644 --- a/Source/Common/ReplayInfo.cs +++ b/Source/Common/ReplayInfo.cs @@ -13,7 +13,6 @@ public class ReplayInfo public int playerFaction; public List sections = new(); - public List events = new(); public string rwVersion; public List modIds; @@ -21,6 +20,7 @@ public class ReplayInfo public List modAssemblyHashes; // Unused, here to satisfy DirectXmlToObject on old saves public XmlBool asyncTime; + public bool multifaction; public static byte[] Write(ReplayInfo info) { @@ -45,10 +45,7 @@ public static ReplayInfo Read(byte[] xml) private static XmlSerializer GetSerializer() { var overrides = new XmlAttributeOverrides(); - overrides.Add(typeof(ReplayInfo), nameof(events), new XmlAttributes - { - XmlArrayItems = { new XmlArrayItemAttribute("li") } - }); + overrides.Add(typeof(ReplayInfo), nameof(sections), new XmlAttributes { XmlArrayItems = { new XmlArrayItemAttribute("li") } @@ -83,12 +80,6 @@ public ReplaySection(int start, int end) } } -public class ReplayEvent -{ - public string name; - public int time; -} - // Taken from StackOverflow, makes bool serialization case-insensitive public struct XmlBool : IXmlSerializable { diff --git a/Source/Common/ServerSettings.cs b/Source/Common/ServerSettings.cs index 31414c7f..eef436fb 100644 --- a/Source/Common/ServerSettings.cs +++ b/Source/Common/ServerSettings.cs @@ -15,6 +15,8 @@ public class ServerSettings public bool direct; public bool lan = true; public bool arbiter; + public bool asyncTime; + public bool multifaction; public bool debugMode; public bool desyncTraces = true; public bool syncConfigs = true; @@ -38,6 +40,8 @@ public void ExposeData() ScribeLike.Look(ref steam, "steam"); ScribeLike.Look(ref direct, "direct"); ScribeLike.Look(ref lan, "lan", true); + ScribeLike.Look(ref debugMode, "asyncTime"); + ScribeLike.Look(ref debugMode, "multifaction"); ScribeLike.Look(ref debugMode, "debugMode"); ScribeLike.Look(ref desyncTraces, "desyncTraces", true); ScribeLike.Look(ref syncConfigs, "syncConfigs", true); diff --git a/Source/Common/Util/CompilerTypes.cs b/Source/Common/Util/CompilerTypes.cs index 3ff82c52..1173d320 100644 --- a/Source/Common/Util/CompilerTypes.cs +++ b/Source/Common/Util/CompilerTypes.cs @@ -17,3 +17,18 @@ public AsyncMethodBuilderAttribute(Type builderType) } } } + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NotNullWhenAttribute : Attribute + { + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + public bool ReturnValue { get; } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] + internal sealed class NotNullAttribute : Attribute + { + } +} diff --git a/Source/Common/Util/DynDelegate.cs b/Source/Common/Util/DynDelegate.cs index 0ae63ffb..1a7ddb71 100644 --- a/Source/Common/Util/DynDelegate.cs +++ b/Source/Common/Util/DynDelegate.cs @@ -289,4 +289,4 @@ public static T Create(MethodInfo method, MethodInfo converter = null)// wher }*/ } -} \ No newline at end of file +} diff --git a/Source/Common/Util/Endpoints.cs b/Source/Common/Util/Endpoints.cs index fc621b78..85abbbab 100644 --- a/Source/Common/Util/Endpoints.cs +++ b/Source/Common/Util/Endpoints.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; namespace Multiplayer.Common.Util @@ -6,7 +7,7 @@ namespace Multiplayer.Common.Util public static class Endpoints { // From IPEndPoint in .NET Core - public static bool TryParse(string s, uint defaultPort, out IPEndPoint result) + public static bool TryParse(string s, uint defaultPort, [NotNullWhen(true)] out IPEndPoint? result) { s = s.Trim(); int addressLength = s.Length; diff --git a/Source/Common/Util/Extensions.cs b/Source/Common/Util/Extensions.cs index 837ece37..384cb7d3 100644 --- a/Source/Common/Util/Extensions.cs +++ b/Source/Common/Util/Extensions.cs @@ -17,7 +17,7 @@ public static class Extensions return t.IsValueType ? Activator.CreateInstance(t) : null; } - public static V AddOrGet(this Dictionary dict, K key, Func defaultValueGetter) + public static V GetOrAdd(this Dictionary dict, K key, Func defaultValueGetter) { if (!dict.TryGetValue(key, out V value)) { @@ -30,7 +30,7 @@ public static V AddOrGet(this Dictionary dict, K key, Func def public static V GetOrAddNew(this Dictionary dict, K obj) where V : new() { - return AddOrGet(dict, obj, k => new V()); + return GetOrAdd(dict, obj, k => new V()); } public static IEnumerable ToEnumerable(this T input) diff --git a/Source/Common/Util/HotSwappableAttribute.cs b/Source/Common/Util/HotSwappableAttribute.cs deleted file mode 100644 index 5b6d300f..00000000 --- a/Source/Common/Util/HotSwappableAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Multiplayer.Common.Util; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class HotSwappableAttribute : Attribute -{ -} diff --git a/Source/Common/Util/ZipExtensions.cs b/Source/Common/Util/ZipExtensions.cs index 2ce4c1e2..41b3f337 100644 --- a/Source/Common/Util/ZipExtensions.cs +++ b/Source/Common/Util/ZipExtensions.cs @@ -6,7 +6,6 @@ namespace Multiplayer.Common.Util; -[HotSwappable] public static class ZipExtensions { public static byte[] GetBytes(this ZipArchive zip, string path) diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 4ac51a3e..88d4297c 100644 --- a/Source/Common/Version.cs +++ b/Source/Common/Version.cs @@ -2,8 +2,8 @@ namespace Multiplayer.Common { public static class MpVersion { - public const string Version = "0.8"; - public const int Protocol = 32; + public const string Version = "0.9"; + public const int Protocol = 34; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/Common/WorldData.cs b/Source/Common/WorldData.cs index c868155b..a9bc96d3 100644 --- a/Source/Common/WorldData.cs +++ b/Source/Common/WorldData.cs @@ -6,7 +6,8 @@ namespace Multiplayer.Common; public class WorldData { - public int defaultFactionId; + public int hostFactionId; + public int spectatorFactionId; public byte[]? savedGame; // Compressed game save public byte[]? semiPersistent; // Compressed semi persistent data public Dictionary mapData = new(); // Map id to compressed map data diff --git a/Source/Multiplayer.sln b/Source/Multiplayer.sln index 69ea40c4..ed00c539 100644 --- a/Source/Multiplayer.sln +++ b/Source/Multiplayer.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerLoader", "MultiplayerLoader\MultiplayerLoader.csproj", "{D9F53E9B-A72C-4183-B549-63B649320B61}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsOnMono", "TestsOnMono\TestsOnMono.csproj", "{B9258593-604D-4862-96EF-192B2BC6250E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {D9F53E9B-A72C-4183-B549-63B649320B61}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9F53E9B-A72C-4183-B549-63B649320B61}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9F53E9B-A72C-4183-B549-63B649320B61}.Release|Any CPU.Build.0 = Release|Any CPU + {B9258593-604D-4862-96EF-192B2BC6250E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9258593-604D-4862-96EF-192B2BC6250E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9258593-604D-4862-96EF-192B2BC6250E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9258593-604D-4862-96EF-192B2BC6250E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/MultiplayerLoader/MultiplayerLoader.cs b/Source/MultiplayerLoader/MultiplayerLoader.cs index 68b06ccc..84297553 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.cs +++ b/Source/MultiplayerLoader/MultiplayerLoader.cs @@ -11,8 +11,8 @@ namespace MultiplayerLoader // This class has to be named Multiplayer for backwards compatibility of the settings file location public class Multiplayer : Mod { - public static Multiplayer? instance; - public static Action? settingsWindowDrawer; + public static Multiplayer instance; + public static Action settingsWindowDrawer; public Multiplayer(ModContentPack content) : base(content) { @@ -45,7 +45,6 @@ in ModContentPack.GetAllFilesForModPreserveOrder(Content, "AssembliesCustom/", { byte[] rawSymbolStore = File.ReadAllBytes(fileInfo.FullName); assembly = AppDomain.CurrentDomain.Load(rawAssembly, rawSymbolStore); - Log.Message(""+AssemblyName.GetAssemblyName(item.FullName)); } else { diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index 30b8b4ad..879b8ceb 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -7,18 +7,14 @@ false false MultiplayerLoader - ..\..\Assemblies\ - - - - + diff --git a/Source/Server/Server.cs b/Source/Server/Server.cs index 2f545e1e..eac40859 100644 --- a/Source/Server/Server.cs +++ b/Source/Server/Server.cs @@ -51,10 +51,11 @@ static void LoadSave(MultiplayerServer server, string path) ServerLog.Detail($"Loading {path} saved in RW {replayInfo.rwVersion} with {replayInfo.modNames.Count} mods"); server.settings.gameName = replayInfo.name; - server.worldData.defaultFactionId = replayInfo.playerFaction; + server.worldData.hostFactionId = replayInfo.playerFaction; // todo this assumes only one zero-tick section and map id 0 server.gameTimer = replayInfo.sections[0].start; + server.startingTimer = replayInfo.sections[0].start; server.worldData.savedGame = Compress(zip.GetBytes("world/000_save")); server.worldData.mapData[0] = Compress(zip.GetBytes("maps/000_0_save")); diff --git a/Source/Tests/Tests.csproj b/Source/Tests/Tests.csproj index 0ec55c7f..64444ef5 100644 --- a/Source/Tests/Tests.csproj +++ b/Source/Tests/Tests.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + 11 false diff --git a/Source/TestsOnMono/Program.cs b/Source/TestsOnMono/Program.cs new file mode 100644 index 00000000..ae06c9bb --- /dev/null +++ b/Source/TestsOnMono/Program.cs @@ -0,0 +1,25 @@ +using System; +using Multiplayer.Client; +using Multiplayer.Client.Desyncs; + +class Program +{ + public static unsafe void Main(string[] args) + { + Native.mini_parse_debug_option("disable_omit_fp"); + Native.InitLmfPtr(Native.NativeOS.Dummy); + Native.EarlyInit(Native.NativeOS.Dummy); + + Test(); + } + + public static void Test() + { + int hash = 0; + long[] trace = new long[32]; + DeferredStackTracingImpl.TraceImpl(trace, ref hash); + + foreach (long l in trace) + Console.WriteLine(Native.MethodNameFromAddr(l, false)); + } +} diff --git a/Source/TestsOnMono/TestsOnMono.csproj b/Source/TestsOnMono/TestsOnMono.csproj new file mode 100644 index 00000000..0c139e47 --- /dev/null +++ b/Source/TestsOnMono/TestsOnMono.csproj @@ -0,0 +1,15 @@ + + + + Exe + net48 + enable + 11 + true + + + + + + + From 67e613b7fee761902f1f37281d1e4aa627da4e82 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Mon, 16 Oct 2023 20:46:29 +0200 Subject: [PATCH 2/5] Clean up testing code Use the multifaction hosting option Fix hosting from singleplayer saves --- Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 8 +-- Source/Client/Debug/DebugPatches.cs | 4 +- Source/Client/Debug/DebugTools.cs | 2 +- Source/Client/EarlyInit.cs | 18 +------ Source/Client/Factions/MultifactionPatches.cs | 40 +------------- Source/Client/Multiplayer.cs | 2 +- Source/Client/MultiplayerSession.cs | 4 +- Source/Client/Networking/HostUtil.cs | 2 +- .../Networking/State/ClientLoadingState.cs | 2 - Source/Client/Patches/Optimizations.cs | 52 ------------------- Source/Client/Patches/Patches.cs | 12 +++++ Source/Client/Saving/LoadPatch.cs | 2 +- Source/Client/Saving/Loader.cs | 8 --- Source/Client/Saving/SaveLoad.cs | 6 ++- Source/Client/Saving/ScribeUtil.cs | 12 ++--- Source/Client/Util/MpUtil.cs | 4 +- Source/Client/Windows/ServerBrowser.cs | 7 ++- Source/Common/PlayerManager.cs | 4 +- 18 files changed, 43 insertions(+), 146 deletions(-) delete mode 100644 Source/Client/Patches/Optimizations.cs diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index d3f32336..81e29b87 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -142,12 +142,14 @@ public void PreContext() Rand.PushState(); Rand.StateCompressed = randState; - FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction); + if (Multiplayer.GameComp.multifaction) + FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction); } public void PostContext() { - FactionExtensions.PopFaction(); + if (Multiplayer.GameComp.multifaction) + FactionExtensions.PopFaction(); randState = Rand.StateCompressed; Rand.PopState(); @@ -237,7 +239,7 @@ public void ExecuteCmd(ScheduledCommand cmd) private static void CreateJoinPointAndSendIfHost() { - Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); + Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload(), Multiplayer.GameComp.multifaction); if (!TickPatch.Simulating && !Multiplayer.IsReplay && (Multiplayer.LocalServer != null || Multiplayer.arbiterInstance)) diff --git a/Source/Client/Debug/DebugPatches.cs b/Source/Client/Debug/DebugPatches.cs index 6b76bc2c..c577b5d8 100644 --- a/Source/Client/Debug/DebugPatches.cs +++ b/Source/Client/Debug/DebugPatches.cs @@ -177,8 +177,8 @@ static void Prefix(ref string text) { // On Windows, Debug.Log used by Verse.Log replaces \n with \r\n // Without this patch printing \r\n results in \r\r\n - // if (Native.Windows) - // text = text?.Replace("\r\n", "\n"); + if (Application.platform == RuntimePlatform.WindowsPlayer) + text = text?.Replace("\r\n", "\n"); } } diff --git a/Source/Client/Debug/DebugTools.cs b/Source/Client/Debug/DebugTools.cs index 967d93ab..700dc566 100644 --- a/Source/Client/Debug/DebugTools.cs +++ b/Source/Client/Debug/DebugTools.cs @@ -153,7 +153,7 @@ public static void SendCmd(DebugSource source, int hash, string path, Map map) public static DebugActionNode RecreateGraphAndGetNode(string path) { // Some actions (like quest generation) invoke the RNG during global graph caching - // so we it recreate to avoid desyncs + // so we recreate it to avoid desyncs Dialog_Debug.ResetStaticData(); Dialog_Debug.TrySetupNodeGraph(); diff --git a/Source/Client/EarlyInit.cs b/Source/Client/EarlyInit.cs index a97839d0..6668e6c8 100644 --- a/Source/Client/EarlyInit.cs +++ b/Source/Client/EarlyInit.cs @@ -63,25 +63,9 @@ internal static void InitSync() Sync.ValidateAll(); } - internal static void LatePatches(Harmony harmony) + internal static void LatePatches() { - // optimization, cache DescendantThingDefs - // harmony.PatchMeasure( - // AccessTools.Method(typeof(ThingCategoryDef), "get_DescendantThingDefs"), - // new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Prefix"), - // new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Postfix") - // ); - - // optimization, cache ThisAndChildCategoryDefs - // harmony.PatchMeasure( - // AccessTools.Method(typeof(ThingCategoryDef), "get_ThisAndChildCategoryDefs"), - // new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Prefix"), - // new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Postfix") - // ); - if (MpVersion.IsDebug) - { Log.Message("== Structure == \n" + SyncDict.syncWorkers.PrintStructure()); - } } } diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index dd836a38..a720d66b 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -77,17 +77,10 @@ static bool Prefix(MapParent __instance) } } -// todo this is temporary -[HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GoodwillManagerTick))] -static class GoodwillManagerTickCancel -{ - static bool Prefix() => false; -} - [HarmonyPatch(typeof(Settlement), nameof(Settlement.Attackable), MethodType.Getter)] static class SettlementAttackablePatch { - static bool Prefix() => false; // todo should only be player + static bool Prefix(Settlement __instance) => __instance.Faction is not { IsPlayer: true }; } [HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] @@ -108,37 +101,6 @@ static bool Prefix(Settlement __instance, ref Material __result) [HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] static class SettlementNullFactionPatch2 { - static void OnCodeReload(int version) - { - var harmony = new Harmony("mptestpatches"); - harmony.UnpatchAll("mptestpatches"); - - static bool GoodwillPrefix(Faction other) - { - return other is not { IsPlayer: true }; - } - - static void PrintPrefix() - { - Log.Message("add pawn"); - } - - harmony.Patch( - MethodOf.Inner((GoodwillSituationManager m) => m.GetNaturalGoodwill), - MethodOf.Lambda(GoodwillPrefix).Harmony() - ); - - harmony.Patch( - MethodOf.Inner((GoodwillSituationManager m) => m.GetMaxGoodwill), - MethodOf.Lambda(GoodwillPrefix).Harmony() - ); - - harmony.Patch( - MethodOf.Inner((WorldPawns w) => w.AddPawn(null)), - MethodOf.Lambda(PrintPrefix).Harmony() - ); - } - static bool Prefix(Settlement __instance, ref Texture2D __result) { if (__instance.factionInt == null) diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 37ecc15c..2798dbea 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -121,7 +121,7 @@ public static void InitMultiplayer() LongEventHandler.ExecuteWhenFinished(() => { // Double Execute ensures it'll run last. - LongEventHandler.ExecuteWhenFinished(() => EarlyInit.LatePatches(harmony)); + LongEventHandler.ExecuteWhenFinished(EarlyInit.LatePatches); }); #if DEBUG diff --git a/Source/Client/MultiplayerSession.cs b/Source/Client/MultiplayerSession.cs index 32d50c1c..53c087cf 100644 --- a/Source/Client/MultiplayerSession.cs +++ b/Source/Client/MultiplayerSession.cs @@ -268,7 +268,9 @@ public static void SaveGameToFile_Overwrite(string fileNameNoExtension, bool cur { new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.zip")).Delete(); Replay.ForSaving(fileNameNoExtension).WriteData( - currentReplay ? Multiplayer.session.dataSnapshot : SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData()) + currentReplay ? + Multiplayer.session.dataSnapshot : + SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData(), false) ); Messages.Message("MpGameSaved".Translate(fileNameNoExtension), MessageTypeDefOf.SilentInput, false); Multiplayer.session.lastSaveAt = Time.realtimeSinceStartup; diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 2d4b8be8..308b6287 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -117,7 +117,7 @@ private static void SetGameState(ServerSettings settings) private static async Task CreateGameData() { await LongEventTask.ContinueInLongEvent("MpSaving", false); - return SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); + return SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload(), Multiplayer.GameComp.multifaction); } private static void SetupGameFromSingleplayer() diff --git a/Source/Client/Networking/State/ClientLoadingState.cs b/Source/Client/Networking/State/ClientLoadingState.cs index 503083f5..b7997d6e 100644 --- a/Source/Client/Networking/State/ClientLoadingState.cs +++ b/Source/Client/Networking/State/ClientLoadingState.cs @@ -72,8 +72,6 @@ public void HandleWorldData(ByteReader data) mapsToLoad.Add(mapId); } - //mapsToLoad.RemoveAt(Multiplayer.LocalServer != null ? 1 : 0); // todo dbg - Session.dataSnapshot = new GameDataSnapshot( 0, worldData, diff --git a/Source/Client/Patches/Optimizations.cs b/Source/Client/Patches/Optimizations.cs deleted file mode 100644 index 6a3299fe..00000000 --- a/Source/Client/Patches/Optimizations.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using Verse; - -namespace Multiplayer.Client -{ - static class ThingCategoryDef_DescendantThingDefsPatch - { - static ConcurrentDictionary> values = new(DefaultComparer.Instance); - - static bool Prefix(ThingCategoryDef __instance) - { - return !values.ContainsKey(__instance); - } - - static void Postfix(ThingCategoryDef __instance, ref IEnumerable __result) - { - if (values.TryGetValue(__instance, out HashSet set)) - { - __result = set; - return; - } - - set = new HashSet(__result, DefaultComparer.Instance); - values[__instance] = set; - __result = set; - } - } - - static class ThingCategoryDef_ThisAndChildCategoryDefsPatch - { - static ConcurrentDictionary> values = new(DefaultComparer.Instance); - - static bool Prefix(ThingCategoryDef __instance) - { - return !values.ContainsKey(__instance); - } - - static void Postfix(ThingCategoryDef __instance, ref IEnumerable __result) - { - if (values.TryGetValue(__instance, out HashSet set)) - { - __result = set; - return; - } - - set = new HashSet(__result, DefaultComparer.Instance); - values[__instance] = set; - __result = set; - } - } -} diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 8a1ac633..175062bf 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -546,4 +546,16 @@ static IEnumerable Transpiler(IEnumerable inst return list; } } + + [HarmonyPatch(typeof(PawnTextureAtlas), MethodType.Constructor)] + static class PawnTextureAtlasCtorPatch + { + static void Postfix(PawnTextureAtlas __instance) + { + // Pawn ids can change during deserialization when fixing local (negative) ids in CrossRefHandler_Clear_Patch + __instance.frameAssignments = new Dictionary( + IdentityComparer.Instance + ); + } + } } diff --git a/Source/Client/Saving/LoadPatch.cs b/Source/Client/Saving/LoadPatch.cs index 7eb4b512..16b72cf2 100644 --- a/Source/Client/Saving/LoadPatch.cs +++ b/Source/Client/Saving/LoadPatch.cs @@ -20,7 +20,7 @@ static bool Prefix() try { - ScribeUtil.StartLoading(gameToLoad.SaveData); + ScribeUtil.InitFromXmlDoc(gameToLoad.SaveData); ScribeMetaHeaderUtility.LoadGameDataHeader(ScribeMetaHeaderUtility.ScribeHeaderMode.Map, false); Scribe.EnterNode("game"); Current.Game = new Game(); diff --git a/Source/Client/Saving/Loader.cs b/Source/Client/Saving/Loader.cs index 6b479048..b7072e8c 100644 --- a/Source/Client/Saving/Loader.cs +++ b/Source/Client/Saving/Loader.cs @@ -63,10 +63,6 @@ private static void PostLoad(bool forceAsyncTime) ); Multiplayer.game.myFactionLoading = null; - // todo temporary - // Current.Game.InitData = new GameInitData() { mapSize = 50 }; - // Find.WindowStack.Add(new Page_SelectStartingSite()); - if (forceAsyncTime) Multiplayer.game.gameComp.asyncTime = true; @@ -86,10 +82,6 @@ private static XmlDocument DataSnapshotToXml(GameDataSnapshot dataSnapshot, List using XmlReader reader = XmlReader.Create(new MemoryStream(dataSnapshot.MapData[map])); XmlNode mapNode = gameDoc.ReadNode(reader); gameNode["maps"].AppendChild(mapNode); - - // todo temporary - // if (gameNode["currentMapIndex"] == null) - // gameNode.AddNode("currentMapIndex", map.ToString()); } return gameDoc; diff --git a/Source/Client/Saving/SaveLoad.cs b/Source/Client/Saving/SaveLoad.cs index 7b6129b2..d99dd4e0 100644 --- a/Source/Client/Saving/SaveLoad.cs +++ b/Source/Client/Saving/SaveLoad.cs @@ -184,7 +184,7 @@ public static XmlDocument SaveGameToDoc() return ScribeUtil.FinishWritingToDoc(); } - public static GameDataSnapshot CreateGameDataSnapshot(TempGameData data) + public static GameDataSnapshot CreateGameDataSnapshot(TempGameData data, bool removeCurrentMapId) { XmlNode gameNode = data.SaveData.DocumentElement["game"]; XmlNode mapsNode = gameNode["maps"]; @@ -200,7 +200,9 @@ public static GameDataSnapshot CreateGameDataSnapshot(TempGameData data) mapCmdsDict[id] = new List(Find.Maps.First(m => m.uniqueID == id).AsyncTime().cmds); } - gameNode["currentMapIndex"].RemoveFromParent(); + if (removeCurrentMapId) + gameNode["currentMapIndex"].RemoveFromParent(); + mapsNode.RemoveAll(); byte[] gameData = ScribeUtil.XmlToByteArray(data.SaveData); diff --git a/Source/Client/Saving/ScribeUtil.cs b/Source/Client/Saving/ScribeUtil.cs index 4936ac20..3421fad9 100644 --- a/Source/Client/Saving/ScribeUtil.cs +++ b/Source/Client/Saving/ScribeUtil.cs @@ -66,21 +66,14 @@ public static XmlDocument FinishWritingToDoc() return doc; } - public static void StartLoading(XmlDocument doc) + public static void InitFromXmlDoc(XmlDocument doc) { - loading = true; - ScribeMetaHeaderUtility.loadedGameVersion = VersionControl.CurrentVersionStringWithRev; Scribe.loader.curXmlParent = doc.DocumentElement; Scribe.mode = LoadSaveMode.LoadingVars; } - public static void StartLoading(byte[] data) - { - StartLoading(LoadDocument(data)); - } - public static void FinalizeLoading() { if (!loading) @@ -181,7 +174,8 @@ public static byte[] WriteExposable(IExposable element, string name = RootNode, public static T ReadExposable(byte[] data, Action beforeFinish = null) where T : IExposable { - StartLoading(data); + loading = true; + InitFromXmlDoc(LoadDocument(data)); SupplyCrossRefs(); T element = default; diff --git a/Source/Client/Util/MpUtil.cs b/Source/Client/Util/MpUtil.cs index fbf58145..0514587a 100644 --- a/Source/Client/Util/MpUtil.cs +++ b/Source/Client/Util/MpUtil.cs @@ -177,9 +177,9 @@ IEnumerator IEnumerable.GetEnumerator() } } - public class DefaultComparer : IEqualityComparer + public class IdentityComparer : IEqualityComparer { - public static DefaultComparer Instance = new DefaultComparer(); + public static IdentityComparer Instance = new(); public bool Equals(T x, T y) { diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index bbe35248..48c2b29f 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -19,13 +19,12 @@ namespace Multiplayer.Client { - public class ServerBrowser : Window { private NetManager lanListener; - private List servers = new List(); + private List servers = new(); - public override Vector2 InitialSize => new Vector2(800f, 500f); + public override Vector2 InitialSize => new(800f, 500f); public ServerBrowser() { @@ -53,7 +52,7 @@ public ServerBrowser() private Vector2 lanScroll; private Vector2 steamScroll; - private Vector2 hostScroll; + private static Vector2 hostScroll; private static Tabs tab; enum Tabs diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index 67a9b502..6a99ba97 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -131,7 +131,9 @@ public void OnDesync(ServerPlayer player, int tick, int diffAt) public void OnJoin(ServerPlayer player) { player.hasJoined = true; - player.FactionId = player.id == 0 ? server.worldData.hostFactionId : server.worldData.spectatorFactionId; + player.FactionId = player.id == 0 || !server.settings.multifaction ? + server.worldData.hostFactionId : + server.worldData.spectatorFactionId; server.SendNotification("MpPlayerConnected", player.Username); server.SendChat($"{player.Username} has joined."); From b52e372ea9a73bf92bb75d8b18a2116d50060a48 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Wed, 18 Oct 2023 19:46:26 +0200 Subject: [PATCH 3/5] Add ideo preset chooser for new factions Fix saving of policies (outfits etc) Remove spectator faction when converting to sp --- Source/Client/Comp/World/FactionWorldData.cs | 10 +++- .../Client/Comp/World/MultiplayerWorldComp.cs | 37 +++++++++++- Source/Client/Factions/FactionCreator.cs | 59 +++++++++++++++---- Source/Client/Factions/FactionSidebar.cs | 46 ++++++++++++--- Source/Client/Factions/MultifactionPatches.cs | 7 ++- .../Factions/Page_ChooseIdeo_Multifaction.cs | 49 +++++++++++++++ Source/Client/Factions/SidebarPatch.cs | 7 ++- Source/Client/Networking/HostUtil.cs | 16 +---- .../Networking/State/ClientPlayingState.cs | 3 + Source/Client/Patches/UniqueIds.cs | 30 ++++++++++ Source/Client/Saving/ConvertToSp.cs | 8 +++ Source/Client/UI/IngameDebug.cs | 19 ++---- Source/Client/UI/IngameUI.cs | 23 +++++++- Source/Client/UI/MainMenuPatches.cs | 3 +- 14 files changed, 260 insertions(+), 57 deletions(-) create mode 100644 Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs diff --git a/Source/Client/Comp/World/FactionWorldData.cs b/Source/Client/Comp/World/FactionWorldData.cs index ac715ac7..5d2b9214 100644 --- a/Source/Client/Comp/World/FactionWorldData.cs +++ b/Source/Client/Comp/World/FactionWorldData.cs @@ -32,8 +32,16 @@ public void ExposeData() Scribe_Deep.Look(ref researchSpeed, "researchSpeed"); } - public void Tick() + public void ReassignIds() { + foreach (DrugPolicy p in drugPolicyDatabase.policies) + p.uniqueId = Find.UniqueIDsManager.GetNextThingID(); + + foreach (Outfit o in outfitDatabase.outfits) + o.uniqueId = Find.UniqueIDsManager.GetNextThingID(); + + foreach (FoodRestriction o in foodRestrictionDatabase.foodRestrictions) + o.id = Find.UniqueIDsManager.GetNextThingID(); } public static FactionWorldData New(int factionId) diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index 54a986d2..c2209daa 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -1,6 +1,7 @@ using RimWorld; using RimWorld.Planet; using System.Collections.Generic; +using System.Linq; using Verse; using Multiplayer.Client.Persistent; using Multiplayer.Client.Saving; @@ -36,16 +37,46 @@ public void ExposeData() 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"); + + DoBackCompat(); + } + + private void DoBackCompat() + { + if (Scribe.mode != LoadSaveMode.PostLoadInit) + return; + + if (spectatorFaction == null) { - if (spectatorFaction == null) + void AddSpectatorFaction() { spectatorFaction = HostUtil.AddNewFaction("Spectator", FactionDefOf.PlayerColony); + + factionData[spectatorFaction.loadID] = FactionWorldData.New(spectatorFaction.loadID); + factionData[spectatorFaction.loadID].ReassignIds(); + foreach (var map in Find.Maps) MapSetup.InitNewFactionData(map, spectatorFaction); } - if (trading.RemoveAll(t => t.trader == null || t.playerNegotiator == null) > 0) - Log.Message("Some trading sessions had null entries"); + void RemoveOpponentFaction() + { + // Test faction left in by mistake in version 0.7 + var opponent = + Find.FactionManager.AllFactions.FirstOrDefault(f => f.Name == "Opponent" && f.IsPlayer); + + if (opponent is not null) + { + opponent.RemoveAllRelations(); + Find.FactionManager.allFactions.Remove(opponent); + Log.Warning("Multiplayer removed dummy Opponent faction"); + } + } + + AddSpectatorFaction(); + RemoveOpponentFaction(); } } diff --git a/Source/Client/Factions/FactionCreator.cs b/Source/Client/Factions/FactionCreator.cs index 48d70c2d..7b759ed3 100644 --- a/Source/Client/Factions/FactionCreator.cs +++ b/Source/Client/Factions/FactionCreator.cs @@ -24,7 +24,7 @@ public static void SendPawn(int sessionId, Pawn p) } [SyncMethod] - public static void CreateFaction(int sessionId, string factionName, int tile, string scenario, FactionRelationKind relation) + public static void CreateFaction(int sessionId, string factionName, int tile, string scenario, FactionRelationKind relation, ChooseIdeoInfo chooseIdeoInfo) { PrepareState(sessionId); @@ -32,8 +32,14 @@ public static void CreateFaction(int sessionId, string factionName, int tile, st LongEventHandler.QueueLongEvent(delegate { - int id = Find.UniqueIDsManager.GetNextFactionID(); - var newFaction = NewFaction(id, factionName, FactionDefOf.PlayerColony); + int newFactionID = Find.UniqueIDsManager.GetNextFactionID(); + + var newFaction = NewFaction( + newFactionID, + factionName, + FactionDefOf.PlayerColony, + chooseIdeoInfo + ); newFaction.hidden = true; @@ -44,6 +50,10 @@ public static void CreateFaction(int sessionId, string factionName, int tile, st } FactionContext.Push(newFaction); + + foreach (var pawn in StartingPawnUtility.StartingAndOptionalPawns) + pawn.ideo.SetIdeo(newFaction.ideos.PrimaryIdeo); + var newMap = GenerateNewMap(tile, scenario); FactionContext.Pop(); @@ -94,7 +104,7 @@ private static Map GenerateNewMap(int tile, string scenario) Find.WorldObjects.Add(mapParent); var prevScenario = Find.Scenario; - Current.Game.scenarioInt = ScenarioLister.AllScenarios().First(s => s.name == scenario); + Current.Game.scenarioInt = DefDatabase.AllDefs.First(s => s.defName == scenario).scenario; try { @@ -116,7 +126,7 @@ private static void InitNewGame() ResearchUtility.ApplyPlayerStartingResearch(); } - public static void SetInitialInitData() + public static void SetGameInitData() { Current.Game.InitData = new GameInitData { @@ -127,7 +137,7 @@ public static void SetInitialInitData() public static void PrepareState(int sessionId) { - SetInitialInitData(); + SetGameInitData(); if (pawnStore.TryGetValue(sessionId, out var pawns)) { @@ -140,24 +150,34 @@ public static void PrepareState(int sessionId) } } - private static Faction NewFaction(int id, string name, FactionDef def) + private static Faction NewFaction(int id, string name, FactionDef def, ChooseIdeoInfo chooseIdeoInfo) { Faction faction = Find.FactionManager.AllFactions.FirstOrDefault(f => f.loadID == id); if (faction == null) { - faction = new Faction() { loadID = id, def = def }; - + faction = new Faction { loadID = id, def = def }; faction.ideos = new FactionIdeosTracker(faction); - faction.ideos.ChooseOrGenerateIdeo(new IdeoGenerationParms()); + + if (!ModsConfig.IdeologyActive || Find.IdeoManager.classicMode) + { + faction.ideos.SetPrimary(Faction.OfPlayer.ideos.PrimaryIdeo); + } + else + { + var newIdeo = GenerateIdeo(chooseIdeoInfo); + faction.ideos.SetPrimary(newIdeo); + Find.IdeoManager.Add(newIdeo); + } foreach (Faction other in Find.FactionManager.AllFactionsListForReading) faction.TryMakeInitialRelationsWith(other); Find.FactionManager.Add(faction); - Multiplayer.WorldComp.factionData[faction.loadID] = - FactionWorldData.New(faction.loadID); + var newWorldFactionData = FactionWorldData.New(faction.loadID); + Multiplayer.WorldComp.factionData[faction.loadID] = newWorldFactionData; + newWorldFactionData.ReassignIds(); } faction.Name = name; @@ -165,4 +185,19 @@ private static Faction NewFaction(int id, string name, FactionDef def) return faction; } + + private static Ideo GenerateIdeo(ChooseIdeoInfo chooseIdeoInfo) + { + List list = chooseIdeoInfo.SelectedIdeo.memes.ToList(); + + if (chooseIdeoInfo.SelectedStructure != null) + list.Add(chooseIdeoInfo.SelectedStructure); + else if (DefDatabase.AllDefsListForReading.Where(m => m.category == MemeCategory.Structure && IdeoUtility.IsMemeAllowedFor(m, Find.Scenario.playerFaction.factionDef)).TryRandomElement(out var result)) + list.Add(result); + + Ideo ideo = IdeoGenerator.GenerateIdeo(new IdeoGenerationParms(Find.FactionManager.OfPlayer.def, forceNoExpansionIdeo: false, null, null, list, chooseIdeoInfo.SelectedIdeo.classicPlus, forceNoWeaponPreference: true)); + new Page_ChooseIdeoPreset { selectedStyles = chooseIdeoInfo.SelectedStyles }.ApplySelectedStylesToIdeo(ideo); + + return ideo; + } } diff --git a/Source/Client/Factions/FactionSidebar.cs b/Source/Client/Factions/FactionSidebar.cs index 233e195c..60b68ba3 100644 --- a/Source/Client/Factions/FactionSidebar.cs +++ b/Source/Client/Factions/FactionSidebar.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Text; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Factions; using Multiplayer.Client.Util; using Multiplayer.Common; @@ -66,10 +68,28 @@ private static void DrawFactionCreator() { PreparePawns(); - Find.WindowStack.Add(new Page_ConfigureStartingPawns + var pages = new List(); + Page_ChooseIdeo_Multifaction chooseIdeoPage = null; + + if (ModsConfig.IdeologyActive && !Find.IdeoManager.classicMode) + pages.Add(chooseIdeoPage = new Page_ChooseIdeo_Multifaction()); + + pages.Add(new Page_ConfigureStartingPawns { - nextAct = DoCreateFaction + nextAct = () => + { + DoCreateFaction( + new ChooseIdeoInfo( + chooseIdeoPage?.pageChooseIdeo.selectedIdeo, + chooseIdeoPage?.pageChooseIdeo.selectedStructure, + chooseIdeoPage?.pageChooseIdeo.selectedStyles + ) + ); + } }); + + var page = PageUtility.StitchedPages(pages); + Find.WindowStack.Add(page); } } } @@ -81,7 +101,7 @@ private static void PreparePawns() try { - FactionCreator.SetInitialInitData(); + FactionCreator.SetGameInitData(); // Create starting pawns new ScenPart_ConfigPage_ConfigureStartingPawns { pawnCount = Current.Game.InitData.startingPawnCount } @@ -93,7 +113,7 @@ private static void PreparePawns() } } - private static void DoCreateFaction() + private static void DoCreateFaction(ChooseIdeoInfo chooseIdeoInfo) { int sessionId = Multiplayer.session.playerId; var prevState = Current.programStateInt; @@ -107,8 +127,14 @@ private static void DoCreateFaction() p ); - FactionCreator.CreateFaction(sessionId, factionName, Find.WorldInterface.SelectedTile, - scenario, hostility); + FactionCreator.CreateFaction( + sessionId, + factionName, + Find.WorldInterface.SelectedTile, + scenario, + hostility, + chooseIdeoInfo + ); } finally { @@ -172,3 +198,9 @@ public static bool Button(string text, float width, float height = 35f) return Widgets.ButtonText(Layouter.Rect(width, height), text); } } + +public record ChooseIdeoInfo( + IdeoPresetDef SelectedIdeo, + MemeDef SelectedStructure, + List SelectedStyles +) : ISyncSimple; diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index a720d66b..33db739b 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -29,11 +29,14 @@ static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) foreach (var gizmo in gizmos) yield return gizmo; - if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) + if (Multiplayer.Client == null || Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction) + yield break; + + if (__instance.Faction is { IsPlayer: true } &&__instance.Faction != Faction.OfPlayer) { var otherFaction = __instance.Faction; - yield return new Command_Action() + yield return new Command_Action { defaultLabel = "Change faction relation", icon = MultiplayerStatic.ChangeRelationIcon, diff --git a/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs new file mode 100644 index 00000000..aebfea52 --- /dev/null +++ b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs @@ -0,0 +1,49 @@ +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Factions; + +public class Page_ChooseIdeo_Multifaction : Page +{ + public override string PageTitle => "ChooseYourIdeoligion".Translate(); + + public Page_ChooseIdeoPreset pageChooseIdeo = new(); + + public override void DoWindowContents(Rect inRect) + { + DrawPageTitle(inRect); + float totalHeight = 0f; + Rect mainRect = GetMainRect(inRect); + TaggedString descText = "ChooseYourIdeoligionDesc".Translate(); + float descHeight = Text.CalcHeight(descText, mainRect.width); + Rect descRect = mainRect; + descRect.yMin += totalHeight; + Widgets.Label(descRect, descText); + totalHeight += descHeight + 10f; + + pageChooseIdeo.DrawStructureAndStyleSelection(inRect); + + Rect outRect = mainRect; + outRect.width = 954f; + outRect.yMin += totalHeight; + float num3 = (InitialSize.x - 937f) / 2f; + + Widgets.BeginScrollView( + viewRect: new Rect(0f - num3, 0f, 921f, pageChooseIdeo.totalCategoryListHeight + 100f), + outRect: outRect, + scrollPosition: ref pageChooseIdeo.leftScrollPosition); + + totalHeight = 0f; + pageChooseIdeo.lastCategoryGroupLabel = ""; + foreach (IdeoPresetCategoryDef item in DefDatabase.AllDefsListForReading.Where(c => c != IdeoPresetCategoryDefOf.Classic && c != IdeoPresetCategoryDefOf.Custom && c != IdeoPresetCategoryDefOf.Fluid)) + { + pageChooseIdeo.DrawCategory(item, ref totalHeight); + } + pageChooseIdeo.totalCategoryListHeight = totalHeight; + Widgets.EndScrollView(); + + DoBottomButtons(inRect); + } +} diff --git a/Source/Client/Factions/SidebarPatch.cs b/Source/Client/Factions/SidebarPatch.cs index ff2e48f7..c551a36f 100644 --- a/Source/Client/Factions/SidebarPatch.cs +++ b/Source/Client/Factions/SidebarPatch.cs @@ -10,15 +10,16 @@ static class UIRootPrefix { static void Postfix() { - // if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction != null && Find.FactionManager != null) - // Find.FactionManager.ofPlayer = Multiplayer.RealPlayerFaction; + if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction != null && Find.FactionManager != null) + Find.FactionManager.ofPlayer = Multiplayer.RealPlayerFaction; Layouter.BeginFrame(); if (Multiplayer.Client != null && Multiplayer.RealPlayerFaction == Multiplayer.WorldComp.spectatorFaction && !TickPatch.Simulating && - Find.WindowStack.WindowOfType() == null + !Multiplayer.IsReplay && + Find.WindowStack.WindowOfType() == null ) Find.WindowStack.ImmediateWindow( "MpWindowFaction".GetHashCode(), diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 308b6287..917688af 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -138,20 +138,11 @@ private static void SetupGameFromSingleplayer() spectator.hidden = true; spectator.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Neutral)); + worldComp.factionData[spectator.loadID] = FactionWorldData.New(spectator.loadID); worldComp.spectatorFaction = spectator; - // todo is this needed? - // foreach (FactionWorldData data in worldComp.factionData.Values) - // { - // foreach (DrugPolicy p in data.drugPolicyDatabase.policies) - // p.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - // - // foreach (Outfit o in data.outfitDatabase.outfits) - // o.uniqueId = Multiplayer.GlobalIdBlock.NextId(); - // - // foreach (FoodRestriction o in data.foodRestrictionDatabase.foodRestrictions) - // o.id = Multiplayer.GlobalIdBlock.NextId(); - // } + foreach (FactionWorldData data in worldComp.factionData.Values) + data.ReassignIds(); foreach (Map map in Find.Maps) { @@ -177,7 +168,6 @@ public static Faction AddNewFaction(string name, FactionDef def) faction.def = def; Find.FactionManager.Add(faction); - Multiplayer.WorldComp.factionData[faction.loadID] = FactionWorldData.New(faction.loadID); return faction; } diff --git a/Source/Client/Networking/State/ClientPlayingState.cs b/Source/Client/Networking/State/ClientPlayingState.cs index e10f6e27..b1e031fc 100644 --- a/Source/Client/Networking/State/ClientPlayingState.cs +++ b/Source/Client/Networking/State/ClientPlayingState.cs @@ -263,7 +263,10 @@ public void HandleSetFaction(ByteReader data) Session.GetPlayerInfo(player).factionId = factionId; if (Session.playerId == player) + { Multiplayer.game.ChangeRealPlayerFaction(factionId); + Session.myFactionId = factionId; + } } } diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index d8055a3e..6c52c939 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -43,6 +43,36 @@ static IEnumerable TargetMethods() static bool Prefix() => Scribe.mode != LoadSaveMode.LoadingVars; } + [HarmonyPatch(typeof(OutfitDatabase), nameof(OutfitDatabase.MakeNewOutfit))] + static class OutfitUniqueIdPatch + { + static void Postfix(Outfit __result) + { + if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) + __result.uniqueId = Find.UniqueIDsManager.GetNextThingID(); + } + } + + [HarmonyPatch(typeof(DrugPolicyDatabase), nameof(DrugPolicyDatabase.MakeNewDrugPolicy))] + static class DrugPolicyUniqueIdPatch + { + static void Postfix(DrugPolicy __result) + { + if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) + __result.uniqueId = Find.UniqueIDsManager.GetNextThingID(); + } + } + + [HarmonyPatch(typeof(FoodRestrictionDatabase), nameof(FoodRestrictionDatabase.MakeNewFoodRestriction))] + static class FoodRestrictionUniqueIdPatch + { + static void Postfix(FoodRestriction __result) + { + if (Multiplayer.Ticking || Multiplayer.ExecutingCmds) + __result.id = Find.UniqueIDsManager.GetNextThingID(); + } + } + [HarmonyPatch] static class MessagesMarker { diff --git a/Source/Client/Saving/ConvertToSp.cs b/Source/Client/Saving/ConvertToSp.cs index 63e8ff6b..76bebe9c 100644 --- a/Source/Client/Saving/ConvertToSp.cs +++ b/Source/Client/Saving/ConvertToSp.cs @@ -25,6 +25,14 @@ private static void SaveReplay() private static void PrepareSingleplayer() { Find.GameInfo.permadeathMode = false; + + // Remove spectator faction + var spectator = Multiplayer.WorldComp.spectatorFaction; + if (spectator != null) + { + spectator.RemoveAllRelations(); + Find.FactionManager.allFactions.Remove(spectator); + } } private static void PrepareLoading() diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index 7454be25..fa8f3a5c 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -3,6 +3,7 @@ using HarmonyLib; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; +using Multiplayer.Common; using RimWorld; using UnityEngine; using Verse; @@ -14,10 +15,6 @@ public static class IngameDebug private static double avgDelta; private static double avgTickTime; - public static float tps; - private static float lastTicksAt; - private static int lastTicks; - private const float BtnMargin = 8f; private const float BtnHeight = 27f; private const float BtnWidth = 80f; @@ -51,7 +48,7 @@ internal static void DoDebugPrintout() var async = Find.CurrentMap.AsyncTime(); StringBuilder text = new StringBuilder(); text.Append( - $"{Multiplayer.game.sync.knownClientOpinions.Count} {Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick} {async.mapTicks} {TickPatch.serverFrozen} {TickPatch.frozenAt} "); + $"{Find.IdeoManager.classicMode} {Multiplayer.game.sync.knownClientOpinions.Count} {Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick} {async.mapTicks} {TickPatch.serverFrozen} {TickPatch.frozenAt} "); text.Append( $"z: {Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count()} d: {Find.CurrentMap.designationManager.designationsByDef.Count} hc: {Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}"); @@ -88,7 +85,7 @@ internal static void DoDebugPrintout() : $"\n{Find.WindowStack.focusedWindow}"); text.Append($"\n{UI.CurUICellSize()} {Find.WindowStack.windows.ToStringSafeEnumerable()}"); - text.Append($"\n\nMap TPS: {tps:0.00}"); + text.Append($"\n\nMap TPS: {IngameUIPatch.tps:0.00}"); text.Append($"\nDelta: {Time.deltaTime * 1000f}"); text.Append($"\nAverage ft: {TickPatch.avgFrameTime}"); text.Append($"\nServer tpt: {TickPatch.serverTimePerTick}"); @@ -100,13 +97,6 @@ internal static void DoDebugPrintout() Rect rect1 = new Rect(80f, 170f, 330f, Text.CalcHeight(text.ToString(), 330f)); Widgets.Label(rect1, text.ToString()); - - if (Time.time - lastTicksAt > 0.5f) - { - tps = (tps + (async.mapTicks - lastTicks) * 2f) / 2f; - lastTicks = async.mapTicks; - lastTicksAt = Time.time; - } } //if (Event.current.type == EventType.Repaint) @@ -159,7 +149,8 @@ internal static float DoTimeDiffLabel(float y) { float x = UI.screenWidth - BtnWidth - BtnMargin; - if (Multiplayer.Client != null && + if (MpVersion.IsDebug && + Multiplayer.Client != null && !Multiplayer.GameComp.asyncTime && Find.CurrentMap.AsyncTime() != null && Find.CurrentMap.AsyncTime().mapTicks != Multiplayer.AsyncWorldTime.worldTicks) diff --git a/Source/Client/UI/IngameUI.cs b/Source/Client/UI/IngameUI.cs index 84d0563c..d936c74c 100644 --- a/Source/Client/UI/IngameUI.cs +++ b/Source/Client/UI/IngameUI.cs @@ -24,6 +24,11 @@ public static class IngameUIPatch private const float BtnHeight = 27f; private const float BtnWidth = 80f; + public static float tps; + private static float lastTicksAt; + private static int lastTicks; + private static int lastTicksMapId; + static bool Prefix() { Text.Font = GameFont.Small; @@ -32,6 +37,22 @@ static bool Prefix() IngameDebug.DoDebugPrintout(); } + if (Multiplayer.Client != null && Find.CurrentMap != null && Time.time - lastTicksAt > 0.5f) + { + var async = Find.CurrentMap.AsyncTime(); + + if (lastTicksMapId != Find.CurrentMap.uniqueID) + { + lastTicksMapId = Find.CurrentMap.uniqueID; + lastTicks = async.mapTicks; + tps = 0; + } + + tps = (tps + (async.mapTicks - lastTicks) * 2f) / 2f; + lastTicks = async.mapTicks; + lastTicksAt = Time.time; + } + if (Multiplayer.IsReplay && Multiplayer.session.showTimeline || TickPatch.Simulating) ReplayTimeline.DrawTimeline(); @@ -145,7 +166,7 @@ private static void IndicatorInfo(out Color color, out string text, out bool slo } if (!WorldRendererUtility.WorldRenderedNow) - text += $"\n\nCurrent map avg TPS: {IngameDebug.tps:0.00}"; + text += $"\n\nCurrent map avg TPS: {tps:0.00}"; } private static void HandleUiEventsWhenSimulating() diff --git a/Source/Client/UI/MainMenuPatches.cs b/Source/Client/UI/MainMenuPatches.cs index e37838bf..a58cceb5 100644 --- a/Source/Client/UI/MainMenuPatches.cs +++ b/Source/Client/UI/MainMenuPatches.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Multiplayer.Client.Factions; using Multiplayer.Client.Saving; using UnityEngine; using Verse; @@ -113,7 +114,7 @@ static void Prefix(Rect rect, List optList) static void ShowModDebugInfo() { - Find.WindowStack.Add(new DisconnectedWindow(new SessionDisconnectInfo() { specialButtonTranslated = "Special btn"})); + Find.WindowStack.Add(new Page_ChooseIdeo_Multifaction()); return; var info = new RemoteData(); From 999bbb4968040a53ffd77f5f2452fd3fc625eb16 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Wed, 18 Oct 2023 20:50:19 +0200 Subject: [PATCH 4/5] Don't show "Ally" faction relation option --- Source/Client/Factions/MultifactionPatches.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 33db739b..72c31e24 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -42,12 +42,11 @@ static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) icon = MultiplayerStatic.ChangeRelationIcon, action = () => { - List list = new List(); - for (int i = 0; i <= 2; i++) + List list = new List { - var kind = (FactionRelationKind)i; - list.Add(new FloatMenuOption(kind.ToString(), () => { SetFactionRelation(otherFaction, kind); })); - } + new(FactionRelationKind.Hostile.ToString(), () => { SetFactionRelation(otherFaction, FactionRelationKind.Hostile); }), + new(FactionRelationKind.Neutral.ToString(), () => { SetFactionRelation(otherFaction, FactionRelationKind.Neutral); }) + }; Find.WindowStack.Add(new FloatMenu(list)); } From 1982990b401114ff8b284592d13e6f09ffa45060 Mon Sep 17 00:00:00 2001 From: Zetrith Date: Wed, 18 Oct 2023 21:31:14 +0200 Subject: [PATCH 5/5] Fix sync --- Source/Client/Syncing/Game/SyncDelegates.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index 821c763c..41077469 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -73,7 +73,6 @@ public static void Init() SyncMethod.Lambda(typeof(CompRefuelable), nameof(CompRefuelable.CompGetGizmosExtra), 5).SetDebugOnly(); // Set fuel to max SyncMethod.Lambda(typeof(CompShuttle), nameof(CompShuttle.CompGetGizmosExtra), 1); // Toggle autoload - SyncMethod.Lambda(typeof(ShipJob_Wait), nameof(ShipJob_Wait.GetJobGizmos), 1); // Send shuttle SyncDelegate.LocalFunc(typeof(RoyalTitlePermitWorker_CallShuttle), nameof(RoyalTitlePermitWorker_CallShuttle.CallShuttleToCaravan), "Launch").ExposeParameter(1); // Call shuttle permit on caravan