diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index f7424ed8..fe0f168e 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -158,9 +158,11 @@ public void PreContext() { if (Multiplayer.GameComp.multifaction) { - map.PushFaction(map.ParentFaction is { IsPlayer: true } + map.PushFaction( + map.ParentFaction is { IsPlayer: true } ? map.ParentFaction - : Multiplayer.WorldComp.spectatorFaction); + : Multiplayer.WorldComp.spectatorFaction, + force: true); } prevTime = TimeSnapshot.GetAndSetFromMap(map); diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index b4e08915..6859309e 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -142,13 +142,21 @@ public void PreContext() Rand.StateCompressed = randState; if (Multiplayer.GameComp.multifaction) - FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction); + { + FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction, force: true); + foreach (var map in Find.Maps) + map.MpComp().SetFaction(Multiplayer.WorldComp.spectatorFaction); + } } public void PostContext() { if (Multiplayer.GameComp.multifaction) - FactionExtensions.PopFaction(); + { + var f = FactionExtensions.PopFaction(); + foreach (var map in Find.Maps) + map.MpComp().SetFaction(f); + } randState = Rand.StateCompressed; Rand.PopState(); diff --git a/Source/Client/Comp/Game/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs index d7e14a69..2f3909cc 100644 --- a/Source/Client/Comp/Game/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Client.Comp { - public class MultiplayerGameComp : IExposable, IHasSemiPersistentData + public class MultiplayerGameComp : IExposable, IHasSessionData { public bool asyncTime; public bool multifaction; @@ -40,12 +40,12 @@ public void ExposeData() Scribe_Values.Look(ref idBlockBase64, "globalIdBlock"); } - public void WriteSemiPersistent(ByteWriter writer) + public void WriteSessionData(ByteWriter writer) { SyncSerialization.WriteSync(writer, playerData); } - public void ReadSemiPersistent(ByteReader reader) + public void ReadSessionData(ByteReader reader) { playerData = SyncSerialization.ReadSync>(reader); DebugSettings.godMode = LocalPlayerDataOrNull?.godMode ?? false; diff --git a/Source/Client/Comp/Map/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs index 7ddacbfb..24677e5c 100644 --- a/Source/Client/Comp/Map/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -12,14 +12,15 @@ namespace Multiplayer.Client { - public class MultiplayerMapComp : IExposable, IHasSemiPersistentData + public class MultiplayerMapComp : IExposable, IHasSessionData { public static bool tickingFactions; public Map map; - public Dictionary factionData = new(); - public Dictionary customFactionData = new(); + // SortedDictionary to ensure determinism + public SortedDictionary factionData = new(); + public SortedDictionary customFactionData = new(); public SessionManager sessionManager; public List mapDialogs = new(); @@ -156,10 +157,10 @@ private void ExposeFactionData() { if (Scribe.mode == LoadSaveMode.Saving) { - int currentFactionId =GetFactionId(map.zoneManager); + int currentFactionId = GetFactionId(map.zoneManager); Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); - var savedFactionData = new Dictionary(factionData); + var savedFactionData = new SortedDictionary(factionData); savedFactionData.Remove(currentFactionId); Scribe_Custom.LookValueDeep(ref savedFactionData, "factionMapData", map); } @@ -169,8 +170,7 @@ private void ExposeFactionData() Scribe_Values.Look(ref currentFactionId, "currentFactionId"); Scribe_Custom.LookValueDeep(ref factionData, "factionMapData", map); - if (factionData == null) - factionData = new Dictionary(); + factionData ??= new SortedDictionary(); } if (Scribe.mode == LoadSaveMode.LoadingVars) @@ -182,17 +182,17 @@ private void ExposeFactionData() private void ExposeCustomFactionData() { Scribe_Custom.LookValueDeep(ref customFactionData, "customFactionMapData", map); - customFactionData ??= new Dictionary(); + customFactionData ??= new SortedDictionary(); } - public void WriteSemiPersistent(ByteWriter writer) + public void WriteSessionData(ByteWriter writer) { writer.WriteInt32(autosaveCounter); sessionManager.WriteSemiPersistent(writer); } - public void ReadSemiPersistent(ByteReader reader) + public void ReadSessionData(ByteReader reader) { autosaveCounter = reader.ReadInt32(); diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs index b8a3e3ae..c28cd96c 100644 --- a/Source/Client/Comp/World/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -11,7 +11,8 @@ namespace Multiplayer.Client; public class MultiplayerWorldComp : IHasSemiPersistentData { - public Dictionary factionData = new(); + // SortedDictionary to ensure determinism + public SortedDictionary factionData = new(); public World world; @@ -34,8 +35,6 @@ public void ExposeData() { ExposeFactionData(); - Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); - sessionManager.ExposeSessions(); // Ensure a pause lock session exists if there's any pause locks registered if (!PauseLockSession.pauseLocks.NullOrEmpty()) @@ -83,28 +82,31 @@ void RemoveOpponentFaction() private void ExposeFactionData() { + Scribe_References.Look(ref spectatorFaction, "spectatorFaction"); + if (Scribe.mode == LoadSaveMode.Saving) { int currentFactionId = GetFactionId(Find.ResearchManager); Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); - var savedFactionData = new Dictionary(factionData); + var savedFactionData = new SortedDictionary(factionData); savedFactionData.Remove(currentFactionId); - Scribe_Collections.Look(ref savedFactionData, "factionData", LookMode.Value, LookMode.Deep); + Scribe_Custom.LookValueDeep(ref savedFactionData, "factionData"); } else { // The faction whose data is currently set Scribe_Values.Look(ref currentFactionId, "currentFactionId"); - Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); - factionData ??= new Dictionary(); + Scribe_Custom.LookValueDeep(ref factionData, "factionData"); + factionData ??= new SortedDictionary(); } if (Scribe.mode == LoadSaveMode.LoadingVars && Multiplayer.session != null && Multiplayer.game != null) { - Multiplayer.game.myFactionLoading = Find.FactionManager.GetById(Multiplayer.session.myFactionId); + Multiplayer.game.myFactionLoading = + Find.FactionManager.GetById(Multiplayer.session.myFactionId) ?? spectatorFaction; } if (Scribe.mode == LoadSaveMode.LoadingVars) diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index 902c9494..58dcb8f6 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -201,12 +201,6 @@ static void MultiplayerMethodCallLogger(MethodBase __originalMethod) Debug.Log(__originalMethod.FullDescription()); } - [DebugAction(MultiplayerCategory, allowedGameStates = AllowedGameStates.Playing)] - static void Add1000TicksToTime() - { - Find.TickManager.ticksGameInt += 1000; - } - #if DEBUG [DebugOutput] diff --git a/Source/Client/Desyncs/DeferredStackTracing.cs b/Source/Client/Desyncs/DeferredStackTracing.cs index 105eb056..8c299ba3 100644 --- a/Source/Client/Desyncs/DeferredStackTracing.cs +++ b/Source/Client/Desyncs/DeferredStackTracing.cs @@ -2,8 +2,10 @@ using System.Reflection; using HarmonyLib; using Multiplayer.Client.Patches; +using Multiplayer.Common; using RimWorld; using Verse; +using Verse.AI; namespace Multiplayer.Client.Desyncs { @@ -19,7 +21,6 @@ static IEnumerable TargetMethods() { yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Value)); yield return AccessTools.PropertyGetter(typeof(Rand), nameof(Rand.Int)); - //yield return AccessTools.Method(typeof(Thing), nameof(Thing.DeSpawn)); } public static int acc; @@ -57,6 +58,45 @@ public static bool ShouldAddStackTraceForDesyncLog() } } + [HarmonyPatch(typeof(UniqueIDsManager), nameof(UniqueIDsManager.GetNextID))] + public static class UniqueIdsPatch + { + static void Postfix() + { + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.SpawnSetup))] + public static class ThingSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] + public static class ThingDeSpawnPatch + { + static void Postfix(Thing __instance) + { + if (__instance.def.HasThingIDNumber) + DeferredStackTracing.Postfix(); + } + } + + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.EndCurrentJob))] + public static class EndCurrentJobPatch + { + static void Prefix(Pawn_JobTracker __instance) + { + if (MpVersion.IsDebug && __instance.curJob != null && DeferredStackTracing.ShouldAddStackTraceForDesyncLog()) + Multiplayer.game.sync.TryAddInfoForDesyncLog($"EndCurrentJob for {__instance.pawn}: {__instance.curJob}", ""); + } + } + [HarmonyPatch(typeof(WildAnimalSpawner), nameof(WildAnimalSpawner.WildAnimalSpawnerTick))] static class WildAnimalSpawnerTickTraceIgnore { diff --git a/Source/Client/Desyncs/SyncCoordinator.cs b/Source/Client/Desyncs/SyncCoordinator.cs index 33906001..dc3daf91 100644 --- a/Source/Client/Desyncs/SyncCoordinator.cs +++ b/Source/Client/Desyncs/SyncCoordinator.cs @@ -190,7 +190,8 @@ public void TryAddMapRandomState(int map, ulong state) /// /// Logs an item to aid in desync debugging. /// - /// Information to be logged + /// Information to be logged + /// Information to be logged public void TryAddInfoForDesyncLog(string info1, string info2) { if (!ShouldCollect) return; diff --git a/Source/Client/Factions/FactionContext.cs b/Source/Client/Factions/FactionContext.cs index 1697d147..d1f05f8a 100644 --- a/Source/Client/Factions/FactionContext.cs +++ b/Source/Client/Factions/FactionContext.cs @@ -8,9 +8,9 @@ public static class FactionContext { public static Stack stack = new(); - public static Faction Push(Faction newFaction) + public static Faction Push(Faction newFaction, bool force = false) { - if (newFaction == null || Find.FactionManager.ofPlayer == newFaction || !newFaction.def.isPlayer) + if (newFaction == null || !force && Find.FactionManager.ofPlayer == newFaction || !newFaction.def.isPlayer) { stack.Push(null); return null; diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs new file mode 100644 index 00000000..6d6cbe3c --- /dev/null +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -0,0 +1,118 @@ +using System; +using HarmonyLib; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client.Factions; + +[HarmonyPatch(typeof(SettlementUtility), nameof(SettlementUtility.AttackNow))] +static class AttackNowPatch +{ + static void Prefix(Caravan caravan) + { + FactionContext.Push(caravan.Faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[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(FactionIdeosTracker), nameof(FactionIdeosTracker.RecalculateIdeosBasedOnPlayerPawns))] +static class RecalculateFactionIdeosContext +{ + static void Prefix(FactionIdeosTracker __instance) + { + FactionContext.Push(__instance.faction); + } + + static void Finalizer() + { + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(Bill), nameof(Bill.ValidateSettings))] +static class BillValidateSettingsPatch +{ + static void Prefix(Bill __instance) + { + if (Multiplayer.Client == null) return; + FactionContext.Push(__instance.pawnRestriction?.Faction); // todo HostFaction, SlaveFaction? + } + + static void Finalizer() + { + if (Multiplayer.Client == null) return; + FactionContext.Pop(); + } +} + +[HarmonyPatch(typeof(Bill_Production), nameof(Bill_Production.ValidateSettings))] +static class BillProductionValidateSettingsPatch +{ + static void Prefix(Bill_Production __instance, ref Map __state) + { + if (Multiplayer.Client == null) return; + + var zoneManager = __instance.storeZone?.zoneManager ?? __instance.includeFromZone?.zoneManager; + if (__instance.Map != null && zoneManager != null) + { + __instance.Map.PushFaction(zoneManager.map.MpComp().GetFactionId(zoneManager)); + __state = __instance.Map; + } + } + + static void Finalizer(Map __state) + { + __state?.PopFaction(); + } +} diff --git a/Source/Client/Factions/FactionExtensions.cs b/Source/Client/Factions/FactionExtensions.cs index 53ed80b0..e07bfa37 100644 --- a/Source/Client/Factions/FactionExtensions.cs +++ b/Source/Client/Factions/FactionExtensions.cs @@ -8,9 +8,9 @@ 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) + public static void PushFaction(this Map map, Faction f, bool force = false) { - var faction = FactionContext.Push(f); + var faction = FactionContext.Push(f, force); if (faction == null) return; Multiplayer.WorldComp?.SetFaction(faction); @@ -23,9 +23,9 @@ public static void PushFaction(this Map map, int factionId) map.PushFaction(faction); } - public static void PopFaction() + public static Faction PopFaction() { - PopFaction(null); + return PopFaction(null); } public static Faction PopFaction(this Map map) diff --git a/Source/Client/Factions/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs index d7db8d4d..907d5370 100644 --- a/Source/Client/Factions/FactionRepeater.cs +++ b/Source/Client/Factions/FactionRepeater.cs @@ -9,7 +9,7 @@ namespace Multiplayer.Client { static class FactionRepeater { - public static bool Template(Dictionary factionIdToData, Action dataProcessor, Map map, ref bool ignore) + public static bool Template(IDictionary factionIdToData, Action dataProcessor, Map map, ref bool ignore) { if (Multiplayer.Client == null || ignore) return true; diff --git a/Source/Client/Factions/FactionsWindow.cs b/Source/Client/Factions/FactionsWindow.cs new file mode 100644 index 00000000..301980b3 --- /dev/null +++ b/Source/Client/Factions/FactionsWindow.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Factions; + +public class FactionsWindow : Window +{ + public override Vector2 InitialSize { get; } = new(700, 600); + + private static Vector2 scroll; + + public FactionsWindow() + { + doCloseX = true; + } + + public override void DoWindowContents(Rect inRect) + { + var group = DragAndDropWidget.NewGroup(); + + Layouter.BeginArea(inRect); + Layouter.BeginScroll(ref scroll, spacing: 0f); + + var factions = Find.FactionManager.AllFactions.Where(f => f.IsPlayer).ToList(); + + void DrawFactionInLastRect(Faction faction) + { + DragAndDropWidget.DropArea(group, Layouter.LastRect(), playerId => + { + Multiplayer.Client.Send(Packets.Client_SetFaction, (int)playerId, faction.loadID); + }, null); + + Layouter.BeginVerticalInLastRect(spacing: 1f); + + Layouter.BeginHorizontal(); + { + Layouter.BeginVertical(spacing: 0f, false); + Layouter.Rect(0f, 5f); + + if (Layouter.Button(">", 20f, 20f)) + Multiplayer.Client.Send(Packets.Client_SetFaction, Multiplayer.session.playerId, faction.loadID); + + TooltipHandler.TipRegion(Layouter.LastRect(), "Switch faction"); + + Layouter.EndVertical(); + + using (MpStyle.Set(GameFont.Medium)) + Layouter.Label(faction.Name); + } + Layouter.EndHorizontal(); + + foreach (var p in Multiplayer.session.players) + { + if (p.factionId != faction.loadID) + continue; + + var rect = Layouter.ContentRect(p.username); + + if (Multiplayer.LocalServer != null && DragAndDropWidget.Draggable(group, rect, p.id)) + Widgets.Label(rect with { position = Event.current.mousePosition }, p.username); + else + Widgets.Label(rect, p.username); + } + + Layouter.EndVertical(); + } + + for (int i = 0; i < factions.Count; i++) + { + Layouter.BeginHorizontal(); + + float height = 23 * Multiplayer.session.players.Count(p => p.factionId == factions[i].loadID) + 30f; + if (i + 1 < factions.Count) + height = Math.Max(23 * Multiplayer.session.players.Count(p => p.factionId == factions[i+1].loadID) + 30f, height); + height = Math.Max(200, height); + + Layouter.FixedHeight(height); + DrawFactionInLastRect(factions[i]); + i++; + + Layouter.FixedHeight(height); + if (i < factions.Count) DrawFactionInLastRect(factions[i]); + + Layouter.EndHorizontal(); + } + + Layouter.EndScroll(); + Layouter.EndArea(); + } +} diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 14cde5b6..94abfdca 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -11,6 +10,7 @@ using UnityEngine; using Verse; using Verse.AI; +using Verse.Sound; namespace Multiplayer.Client.Patches; @@ -72,20 +72,6 @@ static void SetRelation(Faction other, FactionRelationKind kind) } } -[HarmonyPatch(typeof(SettlementUtility), nameof(SettlementUtility.AttackNow))] -static class AttackNowPatch -{ - static void Prefix(Caravan caravan) - { - FactionContext.Push(caravan.Faction); - } - - static void Finalizer() - { - FactionContext.Pop(); - } -} - [HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] static class CheckDefeatedPatch { @@ -204,66 +190,6 @@ static bool FactionIsPlayer(Faction f) } } -[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(FactionIdeosTracker), nameof(FactionIdeosTracker.RecalculateIdeosBasedOnPlayerPawns))] -static class RecalculateFactionIdeosContext -{ - static void Prefix(FactionIdeosTracker __instance) - { - FactionContext.Push(__instance.faction); - } - - static void Finalizer() - { - FactionContext.Pop(); - } -} - [HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] static class RecacheColonistBelieverCountPatch { @@ -407,6 +333,32 @@ static void Postfix(LetterStack __instance, Letter let) } } +[HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string))] +static class LetterStackReceiveSoundOnlyMyFaction +{ + private static MethodInfo PlayOneShotOnCamera = + typeof(SoundStarter).GetMethod(nameof(SoundStarter.PlayOneShotOnCamera)); + + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand == PlayOneShotOnCamera) + yield return new CodeInstruction( + OpCodes.Call, + SymbolExtensions.GetMethodInfo((SoundDef s, Map m) => PlaySoundReplacement(s, m))); + else + yield return inst; + } + } + + static void PlaySoundReplacement(SoundDef sound, Map map) + { + if (Multiplayer.RealPlayerFaction == Faction.OfPlayer) + sound.PlayOneShotOnCamera(map); + } +} + [HarmonyPatch] static class DontClearDialogBeginRitualCache { @@ -444,43 +396,6 @@ static bool ShouldCancelCacheClear() } } -[HarmonyPatch(typeof(Bill), nameof(Bill.ValidateSettings))] -static class BillValidateSettingsPatch -{ - static void Prefix(Bill __instance) - { - if (Multiplayer.Client == null) return; - FactionContext.Push(__instance.pawnRestriction?.Faction); // todo HostFaction, SlaveFaction? - } - - static void Finalizer() - { - if (Multiplayer.Client == null) return; - FactionContext.Pop(); - } -} - -[HarmonyPatch(typeof(Bill_Production), nameof(Bill_Production.ValidateSettings))] -static class BillProductionValidateSettingsPatch -{ - static void Prefix(Bill_Production __instance, ref Map __state) - { - if (Multiplayer.Client == null) return; - - var zoneManager = __instance.storeZone?.zoneManager ?? __instance.includeFromZone?.zoneManager; - if (__instance.Map != null && zoneManager != null) - { - __instance.Map.PushFaction(zoneManager.map.MpComp().GetFactionId(zoneManager)); - __state = __instance.Map; - } - } - - static void Finalizer(Map __state) - { - __state?.PopFaction(); - } -} - [HarmonyPatch(typeof(Apparel), nameof(Apparel.WornGraphicPath), MethodType.Getter)] static class ApparelWornGraphicPathGetterPatch { diff --git a/Source/Client/Factions/SidebarPatch.cs b/Source/Client/Factions/SidebarPatch.cs index c551a36f..cb2013fd 100644 --- a/Source/Client/Factions/SidebarPatch.cs +++ b/Source/Client/Factions/SidebarPatch.cs @@ -24,7 +24,7 @@ static void Postfix() Find.WindowStack.ImmediateWindow( "MpWindowFaction".GetHashCode(), new Rect(0, UI.screenHeight / 2f - 400 / 2f, 300, 350), - WindowLayer.Super, + WindowLayer.GameUI, () => { FactionSidebar.DrawFactionSidebar(new Rect(0, 0, 300, 350).ContractedBy(15)); diff --git a/Source/Client/MultiplayerSession.cs b/Source/Client/MultiplayerSession.cs index 53c087cf..3f53404b 100644 --- a/Source/Client/MultiplayerSession.cs +++ b/Source/Client/MultiplayerSession.cs @@ -322,7 +322,7 @@ public struct SessionDisconnectInfo public record GameDataSnapshot( int CachedAtTime, byte[] GameData, - byte[] SemiPersistentData, + byte[] SessionData, Dictionary MapData, Dictionary> MapCmds // Global cmds are -1, this is mutated by MultiplayerSession.ScheduleCommand ); diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 5f54dc6d..550864a4 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -12,6 +12,7 @@ using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; +using RimWorld.Planet; using Steamworks; using UnityEngine; using Verse; @@ -403,6 +404,35 @@ void LogError(string str) } } + // Set FactionContext in common WorldObject methods + { + var prefix = new HarmonyMethod(typeof(WorldObjectMethodPatches).GetMethod(nameof(WorldObjectMethodPatches.Prefix))); + var finalizer = new HarmonyMethod(typeof(WorldObjectMethodPatches).GetMethod(nameof(WorldObjectMethodPatches.Finalizer))); + + var thingMethods = new[] + { + ("SpawnSetup", Type.EmptyTypes), + ("Tick", Type.EmptyTypes) + }; + + foreach (Type t in typeof(WorldObject).AllSubtypesAndSelf()) + { + foreach ((string m, Type[] args) in thingMethods) + { + MethodInfo method = t.GetMethod(m, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly, null, args, null); + if (method != null) + { + try + { + harmony.PatchMeasure(method, prefix, finalizer: finalizer); + } catch (Exception e) { + LogError($"FAIL: {method.DeclaringType.FullName}:{method.Name} with {e}"); + } + } + } + } + } + // Full precision floating point saving { var doubleSavePrefix = new HarmonyMethod(typeof(ValueSavePatch).GetMethod(nameof(ValueSavePatch.DoubleSave_Prefix))); diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index d008482e..416af117 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -49,15 +49,15 @@ public static async ClientTask HostServer(ServerSettings settings, bool fromRepl private static void CreateSession(ServerSettings settings) { - var session = new MultiplayerSession(); - if (Multiplayer.session != null) // This is the case when hosting from a replay - session.dataSnapshot = Multiplayer.session.dataSnapshot; + var session = new MultiplayerSession + { + myFactionId = Faction.OfPlayer.loadID, + localServerSettings = settings, + gameName = settings.gameName, + dataSnapshot = Multiplayer.session?.dataSnapshot // This is the case when hosting from a replay + }; Multiplayer.session = session; - - session.myFactionId = Faction.OfPlayer.loadID; - session.localServerSettings = settings; - session.gameName = settings.gameName; } private static void PrepareLocalServer(ServerSettings settings, bool fromReplay) diff --git a/Source/Client/Networking/State/ClientLoadingState.cs b/Source/Client/Networking/State/ClientLoadingState.cs index b7997d6e..f85264c2 100644 --- a/Source/Client/Networking/State/ClientLoadingState.cs +++ b/Source/Client/Networking/State/ClientLoadingState.cs @@ -42,7 +42,7 @@ public void HandleWorldData(ByteReader data) bool serverFrozen = data.ReadBool(); byte[] worldData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); - byte[] semiPersistentData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); + byte[] sessionData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); var mapCmdsDict = new Dictionary>(); var mapDataDict = new Dictionary(); @@ -75,7 +75,7 @@ public void HandleWorldData(ByteReader data) Session.dataSnapshot = new GameDataSnapshot( 0, worldData, - semiPersistentData, + sessionData, mapDataDict, mapCmdsDict ); diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index aea78d6e..cde190f1 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -107,8 +107,12 @@ static bool Prefix() if (MpVersion.IsDebug) SimpleProfiler.Start(); - DoUpdate(out var worked); - if (worked) workTicks++; + RunCmds(); + if (LongEventHandler.eventQueue.Count == 0) + { + DoUpdate(out var worked); + if (worked) workTicks++; + } if (MpVersion.IsDebug) SimpleProfiler.Pause(); @@ -158,6 +162,24 @@ static void Postfix() Shader.SetGlobalFloat(ShaderPropertyIDs.GameSeconds, Find.CurrentMap.AsyncTime().mapTicks.TicksToSeconds()); } + private static bool RunCmds() + { + int curTimer = Timer; + + foreach (ITickable tickable in AllTickables) + { + while (tickable.Cmds.Count > 0 && tickable.Cmds.Peek().ticks == curTimer) + { + ScheduledCommand cmd = tickable.Cmds.Dequeue(); + tickable.ExecuteCmd(cmd); + + if (LongEventHandler.eventQueue.Count > 0) return true; // Yield to e.g. join-point creation + } + } + + return false; + } + public static void DoUpdate(out bool worked) { worked = false; @@ -165,6 +187,8 @@ public static void DoUpdate(out bool worked) while (Simulating ? (Timer < simulating.target && updateTimer.ElapsedMilliseconds < 25) : (ticksToRun > 0)) { + if (RunCmds()) + return; if (DoTick(ref worked)) return; } @@ -180,24 +204,9 @@ public static void DoTicks(int ticks) } // Returns whether the tick loop should stop - public static bool DoTick(ref bool worked, bool justCmds = false) + public static bool DoTick(ref bool worked) { tickTimer.Restart(); - int curTimer = Timer; - - foreach (ITickable tickable in AllTickables) - { - while (tickable.Cmds.Count > 0 && tickable.Cmds.Peek().ticks == curTimer) - { - ScheduledCommand cmd = tickable.Cmds.Dequeue(); - tickable.ExecuteCmd(cmd); - - if (LongEventHandler.eventQueue.Count > 0) return true; // Yield to e.g. join-point creation - } - } - - if (justCmds) - return true; foreach (ITickable tickable in AllTickables) { diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index 6c52c939..a7c36cfb 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -2,7 +2,6 @@ using RimWorld; using System.Collections.Generic; using System.Reflection; -using Multiplayer.Client.Desyncs; using Verse; namespace Multiplayer.Client.Patches @@ -25,8 +24,6 @@ static void Postfix(ref int __result) if (Multiplayer.InInterface || Current.ProgramState == ProgramState.Entry) __result = localIds--; - else - DeferredStackTracing.Postfix(); } } diff --git a/Source/Client/Patches/WorldObjectMethodPatches.cs b/Source/Client/Patches/WorldObjectMethodPatches.cs new file mode 100644 index 00000000..43621dd0 --- /dev/null +++ b/Source/Client/Patches/WorldObjectMethodPatches.cs @@ -0,0 +1,25 @@ +using HarmonyLib; +using RimWorld.Planet; + +namespace Multiplayer.Client.Patches; + +public static class WorldObjectMethodPatches +{ + [HarmonyPriority(MpPriority.MpFirst)] + public static void Prefix(WorldObject __instance) + { + if (Multiplayer.Client == null) return; + + if (__instance.def.canHaveFaction) + FactionContext.Push(__instance.Faction); + } + + [HarmonyPriority(MpPriority.MpLast)] + public static void Finalizer(WorldObject __instance) + { + if (Multiplayer.Client == null) return; + + if (__instance.def.canHaveFaction) + FactionContext.Pop(); + } +} diff --git a/Source/Client/Saving/IHasSemiPersistentData.cs b/Source/Client/Saving/IHasSemiPersistentData.cs deleted file mode 100644 index 1346fc8d..00000000 --- a/Source/Client/Saving/IHasSemiPersistentData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Multiplayer.Common; - -namespace Multiplayer.Client.Saving -{ - public interface IHasSemiPersistentData - { - void WriteSemiPersistent(ByteWriter writer); - void ReadSemiPersistent(ByteReader reader); - } -} diff --git a/Source/Client/Saving/IHasSessionData.cs b/Source/Client/Saving/IHasSessionData.cs new file mode 100644 index 00000000..68beb4a3 --- /dev/null +++ b/Source/Client/Saving/IHasSessionData.cs @@ -0,0 +1,10 @@ +using Multiplayer.Common; + +namespace Multiplayer.Client.Saving +{ + public interface IHasSessionData + { + void WriteSessionData(ByteWriter writer); + void ReadSessionData(ByteReader reader); + } +} diff --git a/Source/Client/Saving/LoadPatch.cs b/Source/Client/Saving/LoadPatch.cs index 16b72cf2..cd249e7d 100644 --- a/Source/Client/Saving/LoadPatch.cs +++ b/Source/Client/Saving/LoadPatch.cs @@ -30,7 +30,7 @@ static bool Prefix() // todo revisit disconnection during loading // todo loading can be async, concurrency issues if (Multiplayer.Client != null) - SemiPersistent.ReadSemiPersistent(gameToLoad.SemiPersistent); + SessionData.ReadSessionData(gameToLoad.SessionData); } finally { diff --git a/Source/Client/Saving/Loader.cs b/Source/Client/Saving/Loader.cs index b7072e8c..04a281c9 100644 --- a/Source/Client/Saving/Loader.cs +++ b/Source/Client/Saving/Loader.cs @@ -14,7 +14,7 @@ public static void ReloadGame(List mapsToLoad, bool changeScene, bool force { var gameDoc = DataSnapshotToXml(Multiplayer.session.dataSnapshot, mapsToLoad); - LoadPatch.gameToLoad = new(gameDoc, Multiplayer.session.dataSnapshot.SemiPersistentData); + LoadPatch.gameToLoad = new(gameDoc, Multiplayer.session.dataSnapshot.SessionData); TickPatch.replayTimeSpeed = TimeSpeed.Paused; if (changeScene) diff --git a/Source/Client/Saving/SaveLoad.cs b/Source/Client/Saving/SaveLoad.cs index d99dd4e0..298e3cc3 100644 --- a/Source/Client/Saving/SaveLoad.cs +++ b/Source/Client/Saving/SaveLoad.cs @@ -13,7 +13,7 @@ namespace Multiplayer.Client { - public record TempGameData(XmlDocument SaveData, byte[] SemiPersistent); + public record TempGameData(XmlDocument SaveData, byte[] SessionData); public static class SaveLoad { @@ -151,8 +151,8 @@ private static void ClearState() public static TempGameData SaveGameData() { var gameDoc = SaveGameToDoc(); - var semiPersistent = SemiPersistent.WriteSemiPersistent(); - return new TempGameData(gameDoc, semiPersistent); + var sessionData = SessionData.WriteSessionData(); + return new TempGameData(gameDoc, sessionData); } public static XmlDocument SaveGameToDoc() @@ -211,7 +211,7 @@ public static GameDataSnapshot CreateGameDataSnapshot(TempGameData data, bool re return new GameDataSnapshot( TickPatch.Timer, gameData, - data.SemiPersistent, + data.SessionData, mapDataDict, mapCmdsDict ); @@ -221,7 +221,7 @@ public static void SendGameData(GameDataSnapshot snapshot, bool async) { var mapsData = new Dictionary(snapshot.MapData); var gameData = snapshot.GameData; - var semiPersistent = snapshot.SemiPersistentData; + var sessionData = snapshot.SessionData; void Send() { @@ -235,7 +235,7 @@ void Send() } writer.WritePrefixedBytes(GZipStream.CompressBuffer(gameData)); - writer.WritePrefixedBytes(GZipStream.CompressBuffer(semiPersistent)); + writer.WritePrefixedBytes(GZipStream.CompressBuffer(sessionData)); byte[] data = writer.ToArray(); diff --git a/Source/Client/Saving/Scribe_Custom.cs b/Source/Client/Saving/Scribe_Custom.cs index c348bc31..f4df3473 100644 --- a/Source/Client/Saving/Scribe_Custom.cs +++ b/Source/Client/Saving/Scribe_Custom.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; using Verse; @@ -33,119 +34,62 @@ public static void LookRect(ref Rect rect, string label) } // Copy of RimWorld's method but with ctor args - public static void LookValueDeep(ref Dictionary dict, string label, params object[] valueCtorArgs) + public static void LookValueDeep(ref SortedDictionary dict, string label, params object[] valueCtorArgs) { - List keysWorkingList = null; - List valuesWorkingList = null; - if (Scribe.EnterNode(label)) { try { - if (Scribe.mode == LoadSaveMode.Saving || Scribe.mode == LoadSaveMode.LoadingVars) - { - keysWorkingList = new List(); - valuesWorkingList = new List(); - } + List keysWorkingList = null; + List valuesWorkingList = null; if (Scribe.mode == LoadSaveMode.Saving) { - foreach (KeyValuePair current in dict) - { - keysWorkingList.Add(current.Key); - valuesWorkingList.Add(current.Value); - } + keysWorkingList = dict.Keys.ToList(); + valuesWorkingList = dict.Values.ToList(); } Scribe_Collections.Look(ref keysWorkingList, "keys", LookMode.Value); Scribe_Collections.Look(ref valuesWorkingList, "values", LookMode.Deep, valueCtorArgs); - if (Scribe.mode == LoadSaveMode.Saving) - { - if (keysWorkingList != null) - { - keysWorkingList.Clear(); - keysWorkingList = null; - } - - if (valuesWorkingList != null) - { - valuesWorkingList.Clear(); - valuesWorkingList = null; - } - } - if (Scribe.mode == LoadSaveMode.LoadingVars) { dict.Clear(); + if (keysWorkingList == null) { Log.Error("Cannot fill dictionary because there are no keys."); + return; } - else if (valuesWorkingList == null) + + if (valuesWorkingList == null) { Log.Error("Cannot fill dictionary because there are no values."); + return; + } + + if (keysWorkingList.Count != valuesWorkingList.Count) + { + Log.Error($"Keys count does not match the values count while loading a dictionary. Some elements will be skipped. keys={keysWorkingList.Count}, values={valuesWorkingList.Count}"); } - else + + int num = Math.Min(keysWorkingList.Count, valuesWorkingList.Count); + for (int i = 0; i < num; i++) { - if (keysWorkingList.Count != valuesWorkingList.Count) + if (keysWorkingList[i] == null) { - Log.Error(string.Concat(new object[] - { - "Keys count does not match the values count while loading a dictionary (maybe keys and values were resolved during different passes?). Some elements will be skipped. keys=", - keysWorkingList.Count, - ", values=", - valuesWorkingList.Count - })); + Log.Error($"Null key while loading dictionary of {typeof(K)} and {typeof(V)}"); + continue; } - int num = Math.Min(keysWorkingList.Count, valuesWorkingList.Count); - for (int i = 0; i < num; i++) + try { - if (keysWorkingList[i] == null) - { - Log.Error(string.Concat(new object[] - { - "Null key while loading dictionary of ", - typeof(K), - " and ", - typeof(V), - "." - })); - } - else - { - try - { - dict.Add(keysWorkingList[i], valuesWorkingList[i]); - } - catch (Exception ex) - { - Log.Error(string.Concat(new object[] - { - "Exception in LookDictionary(node=", - label, - "): ", - ex - })); - } - } + dict.Add(keysWorkingList[i], valuesWorkingList[i]); + } + catch (Exception ex) + { + Log.Error($"Exception in LookDictionary(node={label}): {ex}"); } - } - } - - if (Scribe.mode == LoadSaveMode.PostLoadInit) - { - if (keysWorkingList != null) - { - keysWorkingList.Clear(); - keysWorkingList = null; - } - - if (valuesWorkingList != null) - { - valuesWorkingList.Clear(); - valuesWorkingList = null; } } } diff --git a/Source/Client/Saving/SemiPersistent.cs b/Source/Client/Saving/SessionData.cs similarity index 75% rename from Source/Client/Saving/SemiPersistent.cs rename to Source/Client/Saving/SessionData.cs index 617bd382..63327756 100644 --- a/Source/Client/Saving/SemiPersistent.cs +++ b/Source/Client/Saving/SessionData.cs @@ -4,32 +4,32 @@ namespace Multiplayer.Client.Saving { - // Semi-persistence is the middle ground between lack of persistence and full persistence: + // Session data is the middle ground between no persistence and full persistence: // - Non-persistent data: // Mainly data in caches // Reset/removed during reloading (f.e. when creating a join point) - // - Semi-persistent data: + // - Session data: // Things like ritual sessions and per player god mode status // Serialized into binary using the Sync system // Session-bound: survives a reload, lost when the server is closed // - Persistent data: // Serialized into XML using RimWorld's Scribe system // Save-bound: survives a server restart - public static class SemiPersistent + public static class SessionData { - public static byte[] WriteSemiPersistent() + public static byte[] WriteSessionData() { var writer = new ByteWriter(); try { var gameWriter = new ByteWriter(); - Multiplayer.GameComp.WriteSemiPersistent(gameWriter); + Multiplayer.GameComp.WriteSessionData(gameWriter); writer.WritePrefixedBytes(gameWriter.ToArray()); } catch (Exception e) { - Log.Error($"Exception writing semi-persistent data for game: {e}"); + Log.Error($"Exception writing session data for game: {e}"); } try @@ -49,21 +49,21 @@ public static byte[] WriteSemiPersistent() try { var mapWriter = new ByteWriter(); - map.MpComp().WriteSemiPersistent(mapWriter); + map.MpComp().WriteSessionData(mapWriter); writer.WriteInt32(map.uniqueID); writer.WritePrefixedBytes(mapWriter.ToArray()); } catch (Exception e) { - Log.Error($"Exception writing semi-persistent data for map {map}: {e}"); + Log.Error($"Exception writing session data for map {map}: {e}"); } } return writer.ToArray(); } - public static void ReadSemiPersistent(byte[] data) + public static void ReadSessionData(byte[] data) { if (data.Length == 0) return; @@ -72,11 +72,11 @@ public static void ReadSemiPersistent(byte[] data) try { - Multiplayer.GameComp.ReadSemiPersistent(new ByteReader(gameData)); + Multiplayer.GameComp.ReadSessionData(new ByteReader(gameData)); } catch (Exception e) { - Log.Error($"Exception reading semi-persistent data for game: {e}"); + Log.Error($"Exception reading session data for game: {e}"); } var worldData = reader.ReadPrefixedBytes(); @@ -99,7 +99,7 @@ public static void ReadSemiPersistent(byte[] data) if (map == null) { - Log.Warning($"Multiplayer: Couldn't find map with id {mapId} while reading semi-persistent data."); + Log.Warning($"Multiplayer: Couldn't find map with id {mapId} while reading session data."); continue; } @@ -107,11 +107,11 @@ public static void ReadSemiPersistent(byte[] data) { var mapReader = new ByteReader(mapData); mapReader.MpContext().map = map; - map.MpComp().ReadSemiPersistent(mapReader); + map.MpComp().ReadSessionData(mapReader); } catch (Exception e) { - Log.Error($"Exception reading semi-persistent data for map {map}: {e}"); + Log.Error($"Exception reading session data for map {map}: {e}"); } } } diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index fa8f3a5c..b58702ff 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -57,7 +57,7 @@ internal static void DoDebugPrintout() { int faction = Find.CurrentMap.ParentFaction.loadID; MultiplayerMapComp comp = Find.CurrentMap.MpComp(); - FactionMapData data = comp.factionData.GetValueSafe(faction); + FactionMapData data = comp.factionData.TryGetValue(faction); if (data != null) { diff --git a/Source/Client/UI/Layouter.cs b/Source/Client/UI/Layouter.cs index 59738080..58213e92 100644 --- a/Source/Client/UI/Layouter.cs +++ b/Source/Client/UI/Layouter.cs @@ -105,14 +105,13 @@ private static El GetNextChild() #region Groups private static void PushGroup(El el) { - var parent = currentGroup; - currentGroup = el; - - if (parent != null) + if (currentGroup != null) { - currentGroup.parent = parent; - parent.children.Add(currentGroup); + el.parent = currentGroup; + currentGroup.children.Add(el); } + + currentGroup = el; } private static void PopGroup() @@ -184,6 +183,18 @@ public static void EndHorizontal() PopGroup(); } + public static void BeginHorizontalCenter() + { + BeginHorizontal(); + FlexibleWidth(); + } + + public static void EndHorizontalCenter() + { + FlexibleWidth(); + EndHorizontal(); + } + public static void BeginVertical(float spacing = 10f, bool stretch = true) { if (Event.current.type == EventType.Layout) @@ -192,6 +203,19 @@ public static void BeginVertical(float spacing = 10f, bool stretch = true) currentGroup = GetNextChild(); } + public static void BeginVerticalInLastRect(float spacing = 10f) + { + if (Event.current.type == EventType.Layout) + { + LastEl().parent = currentGroup; + currentGroup = LastEl(); + currentGroup.spacing = spacing; + } else + { + currentGroup = LastEl(); + } + } + public static void EndVertical() { PopGroup(); @@ -204,15 +228,19 @@ public static void BeginScroll(ref Vector2 scrollPos, float spacing = 10f) var outRect = currentGroup!.rect; currentGroup.scroll = true; - Widgets.BeginScrollView( - outRect, - ref scrollPos, - new Rect(0, 0, outRect.width - currentGroup.paddingRight, currentGroup.childrenHeight)); + var viewRect = new Rect(outRect.x, outRect.y, outRect.width - currentGroup.paddingRight, currentGroup.childrenHeight); + + if (currentGroup.paddingRight != 0f) + Widgets.BeginScrollView( + outRect, + ref scrollPos, + viewRect); } public static void EndScroll() { - Widgets.EndScrollView(); + if (currentGroup.paddingRight != 0f) + Widgets.EndScrollView(); EndVertical(); } #endregion @@ -304,12 +332,29 @@ public static Rect FixedWidth(float width) return GetNextChild().rect; } - public static Rect LastRect() + public static Rect FixedHeight(float height) + { + if (Event.current.type == EventType.Layout) + { + currentGroup!.children.Add(new El() + { rect = new Rect(0, 0, 0, height), widthMode = DimensionMode.Stretch, heightMode = DimensionMode.Fixed }); + return DummyRect; + } + + return GetNextChild().rect; + } + + private static El LastEl() { return Event.current.type == EventType.Layout ? - currentGroup!.children.Last().rect : - currentGroup!.children[currentGroup.currentChild - 1].rect; + currentGroup!.children.Last() : + currentGroup!.children[currentGroup.currentChild - 1]; + } + + public static Rect LastRect() + { + return LastEl().rect; } public static Rect GroupRect() @@ -318,6 +363,18 @@ public static Rect GroupRect() } #endregion + #region UI elements + public static void Label(string text, bool inheritHeight = false) + { + GUI.Label(inheritHeight ? FlexibleWidth() : ContentRect(text), text, Text.CurFontStyle); + } + + public static bool Button(string text, float width, float height = 35f) + { + return Widgets.ButtonText(Rect(width, height), text); + } + #endregion + #region Algorithm private static void Layout() { diff --git a/Source/Client/UI/MainMenuPatches.cs b/Source/Client/UI/MainMenuPatches.cs index a58cceb5..ba7e0887 100644 --- a/Source/Client/UI/MainMenuPatches.cs +++ b/Source/Client/UI/MainMenuPatches.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Multiplayer.Client.Factions; using Multiplayer.Client.Saving; using UnityEngine; using Verse; @@ -114,7 +113,6 @@ static void Prefix(Rect rect, List optList) static void ShowModDebugInfo() { - Find.WindowStack.Add(new Page_ChooseIdeo_Multifaction()); return; var info = new RemoteData(); diff --git a/Source/Client/Windows/ChatWindow.cs b/Source/Client/Windows/ChatWindow.cs index c8ed0802..ecf83f6e 100644 --- a/Source/Client/Windows/ChatWindow.cs +++ b/Source/Client/Windows/ChatWindow.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text; +using Multiplayer.Client.Factions; using Multiplayer.Client.Util; using UnityEngine; using Verse; @@ -91,7 +92,9 @@ public override void DoWindowContents(Rect inRect) DrawChat(chat); GUI.BeginGroup(new Rect(chat.xMax + 10f, chat.y, infoWidth, inRect.height)); - DrawInfo(new Rect(0, 0, infoWidth, inRect.height)); + DrawInfo(new Rect(0, 0, infoWidth, inRect.height - 30f)); + if (Widgets.ButtonText(new Rect(50f, inRect.height - 25f, infoWidth - 50f, 25f), "Factions")) + Find.WindowStack.Add(new FactionsWindow()); GUI.EndGroup(); if (KeyBindingDefOf.Cancel.KeyDownEvent && Find.WindowStack.focusedWindow == this) diff --git a/Source/Client/Windows/ConnectingWindow.cs b/Source/Client/Windows/ConnectingWindow.cs index 2fd7d404..6b40c8e4 100644 --- a/Source/Client/Windows/ConnectingWindow.cs +++ b/Source/Client/Windows/ConnectingWindow.cs @@ -11,7 +11,6 @@ public abstract class BaseConnectingWindow : Window, IConnectionStatusListener { public override Vector2 InitialSize => new(400f, 150f); - protected bool IsConnecting => result == null; protected abstract string ConnectingString { get; } public bool returnToServerBrowser; @@ -47,7 +46,7 @@ public override void DoWindowContents(Rect inRect) else if (Multiplayer.Client?.StateObj is ClientLoadingState { subState: LoadingState.Downloading }) label = "MpDownloading".Translate(Multiplayer.Client.FragmentProgress); else - label = IsConnecting ? (ConnectingString + MpUI.FixedEllipsis()) : result; + label = result ?? (ConnectingString + MpUI.FixedEllipsis()); const float buttonHeight = 40f; const float buttonWidth = 120f; @@ -85,7 +84,8 @@ public class RejoiningWindow : BaseConnectingWindow public class ConnectingWindow : BaseConnectingWindow { - protected override string ConnectingString => string.Format("MpConnectingTo".Translate("{0}", port), address); + protected override string ConnectingString => + string.Format("MpConnectingTo".Translate("{0}", port), address); private string address; private int port; diff --git a/Source/Client/Windows/ModCompatWindow.cs b/Source/Client/Windows/ModCompatWindow.cs index 4dc0e628..f820a8ca 100644 --- a/Source/Client/Windows/ModCompatWindow.cs +++ b/Source/Client/Windows/ModCompatWindow.cs @@ -12,7 +12,6 @@ namespace Multiplayer.Client { - public class ModCompatWindow : Window { public override Vector2 InitialSize => popup ? new(600, 450) : new(900, 600); @@ -354,7 +353,7 @@ int GetScoreForSorting(ModMetaData mod) modsActive = Order(ModsConfig.ActiveModsInLoadOrder, sort).ToList(); modsInstalled = Order( - ModLister.AllInstalledMods.Where(m => !m.Active && m.VersionCompatible), + ModLister.AllInstalledMods.Where(m => !m.Active && m.VersionCompatible).OrderBy(m => m.Name), forceNameSort && sort.d == SortDirection.None ? (SortType.Name, SortDirection.Ascending) : sort ).ToList(); diff --git a/Source/Common/CommandHandler.cs b/Source/Common/CommandHandler.cs index 1aa43286..f9fad36b 100644 --- a/Source/Common/CommandHandler.cs +++ b/Source/Common/CommandHandler.cs @@ -15,9 +15,6 @@ public CommandHandler(MultiplayerServer server) public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerPlayer? sourcePlayer = null, ServerPlayer? fauxSource = null) { - if (server.freezeManager.Frozen) - return; - // policy if (sourcePlayer != null) { @@ -39,7 +36,7 @@ public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerP byte[] toSave = ScheduledCommand.Serialize( new ScheduledCommand( cmd, - server.gameTimer + 1, + server.gameTimer, factionId, mapId, sourcePlayer?.id ?? fauxSource?.id ?? ScheduledCommand.NoPlayer, diff --git a/Source/Common/Networking/State/ServerLoadingState.cs b/Source/Common/Networking/State/ServerLoadingState.cs index 95cb59c3..0b503207 100644 --- a/Source/Common/Networking/State/ServerLoadingState.cs +++ b/Source/Common/Networking/State/ServerLoadingState.cs @@ -31,7 +31,7 @@ public void SendWorldData() writer.WriteInt32(Server.commands.SentCmds); writer.WriteBool(Server.freezeManager.Frozen); writer.WritePrefixedBytes(Server.worldData.savedGame); - writer.WritePrefixedBytes(Server.worldData.semiPersistent); + writer.WritePrefixedBytes(Server.worldData.sessionData); writer.WriteInt32(Server.worldData.mapCmds.Count); diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index 203d7ef6..d12da74a 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -100,7 +100,7 @@ public void HandleWorldDataUpload(ByteReader data) } Server.worldData.savedGame = data.ReadPrefixedBytes(); - Server.worldData.semiPersistent = data.ReadPrefixedBytes(); + Server.worldData.sessionData = data.ReadPrefixedBytes(); if (Server.worldData.CreatingJoinPoint) Server.worldData.EndJoinPointCreation(); @@ -261,6 +261,7 @@ public void HandleSetFaction(ByteReader data) var player = Server.GetPlayer(playerId); if (player == null) return; + if (player.FactionId == factionId) return; player.FactionId = factionId; Server.SendToPlaying(Packets.Server_SetFaction, new object[] { playerId, factionId }); diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 9997dd7f..45b33221 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.9.4"; - public const int Protocol = 37; + public const string Version = "0.9.6"; + public const int Protocol = 39; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/Common/WorldData.cs b/Source/Common/WorldData.cs index a9bc96d3..06d88f07 100644 --- a/Source/Common/WorldData.cs +++ b/Source/Common/WorldData.cs @@ -9,7 +9,7 @@ public class WorldData public int hostFactionId; public int spectatorFactionId; public byte[]? savedGame; // Compressed game save - public byte[]? semiPersistent; // Compressed semi persistent data + public byte[]? sessionData; // Compressed semi persistent data public Dictionary mapData = new(); // Map id to compressed map data public Dictionary> mapCmds = new(); // Map id to serialized cmds list diff --git a/Source/Server/Server.cs b/Source/Server/Server.cs index eac40859..3907e97a 100644 --- a/Source/Server/Server.cs +++ b/Source/Server/Server.cs @@ -61,7 +61,7 @@ static void LoadSave(MultiplayerServer server, string path) server.worldData.mapData[0] = Compress(zip.GetBytes("maps/000_0_save")); server.worldData.mapCmds[0] = ScheduledCommand.DeserializeCmds(zip.GetBytes("maps/000_0_cmds")).Select(ScheduledCommand.Serialize).ToList(); server.worldData.mapCmds[-1] = ScheduledCommand.DeserializeCmds(zip.GetBytes("world/000_cmds")).Select(ScheduledCommand.Serialize).ToList(); - server.worldData.semiPersistent = Array.Empty(); + server.worldData.sessionData = Array.Empty(); } static byte[] Compress(byte[] input)