From d365d6ae56662a8d0feb1ed06ee09c8809174014 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sat, 28 Jun 2025 23:27:08 +1000 Subject: [PATCH 01/38] Updated version numbers. --- About/About.xml | 4 ++-- Source/Client/Multiplayer.csproj | 6 +++--- Source/Common/Common.csproj | 4 ++-- Source/MultiplayerLoader/MultiplayerLoader.csproj | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/About/About.xml b/About/About.xml index e7c40d3a..8e190b45 100644 --- a/About/About.xml +++ b/About/About.xml @@ -3,12 +3,12 @@ rwmt.Multiplayer Multiplayer -
  • 1.5
  • +
  • 1.6
  • RimWorld Multiplayer Team https://github.com/rwmt/Multiplayer <b>Important: This mod should be placed right below Core and expansions in the mod list to work properly! -Requires RimWorld >= 1.5.4104</b>\n +Requires RimWorld >= 1.6.4491</b>\n Multiplayer mod for RimWorld. FAQ - https://hackmd.io/@rimworldmultiplayer/docs/ diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index 0c443666..d0db8fa4 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -27,12 +27,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 7f39a9d6..e002ddab 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -12,9 +12,9 @@ - + - + diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj index 5572bda9..b107b842 100644 --- a/Source/MultiplayerLoader/MultiplayerLoader.csproj +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -10,13 +10,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From e449dc5c4f7072810c9cb5967bd3ac038cb2a439 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 15:34:13 +1000 Subject: [PATCH 02/38] Cleared all build and mod loading errors for 1.6. --- Source/Client/AsyncTime/AsyncTimePatches.cs | 4 +- Source/Client/AsyncTime/SetMapTime.cs | 4 +- Source/Client/AsyncTime/TimeControlUI.cs | 4 +- Source/Client/Debug/DebugActions.cs | 2 +- Source/Client/Debug/DebugSync.cs | 8 +- Source/Client/Factions/Blueprints.cs | 67 +++++++++++---- .../Client/Factions/FactionContextSetters.cs | 8 +- Source/Client/Factions/FactionCreator.cs | 4 +- Source/Client/Factions/MultifactionPatches.cs | 10 +-- Source/Client/Patches/AllGroupsPatch.cs | 2 +- Source/Client/Patches/ArbiterPatches.cs | 20 ++--- Source/Client/Patches/HashCodes.cs | 7 +- Source/Client/Patches/LongEvents.cs | 6 +- Source/Client/Patches/Patches.cs | 9 ++- Source/Client/Patches/Seeds.cs | 3 +- Source/Client/Patches/ThingMethodPatches.cs | 2 +- Source/Client/Patches/TickPatch.cs | 2 +- Source/Client/Patches/TileTemperatures.cs | 14 ++-- .../Persistent/CaravanFormingPatches.cs | 10 +-- Source/Client/Persistent/Trading.cs | 2 +- Source/Client/Saving/CacheForReloading.cs | 81 ++++++++++--------- Source/Client/Saving/SaveLoad.cs | 9 ++- Source/Client/Syncing/Dict/SyncDictMisc.cs | 30 +++++++ .../Client/Syncing/Dict/SyncDictRimWorld.cs | 32 ++++++++ Source/Client/Syncing/Game/SyncActions.cs | 8 +- Source/Client/Syncing/Game/SyncDelegates.cs | 74 ++++++++++++----- Source/Client/Syncing/Game/SyncMethods.cs | 28 ++++--- Source/Client/Syncing/SyncUtil.cs | 5 +- Source/Client/UI/IngameUI.cs | 2 +- Source/Client/UI/LocationPings.cs | 4 +- Source/Client/UI/PlayerCursors.cs | 6 +- Source/Client/Util/FieldRef.cs | 10 +++ Source/Client/Util/SimpleProfiler.cs | 6 +- Source/Server/Server.csproj | 2 +- Source/Tests/Tests.csproj | 2 +- Source/TestsOnMono/TestsOnMono.csproj | 2 +- 36 files changed, 327 insertions(+), 162 deletions(-) create mode 100644 Source/Client/Util/FieldRef.cs diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index 85b50ef4..67638379 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -130,7 +130,7 @@ static void Postfix(ref float __result) { if (PreDrawCalcMarker.calculating == null) return; if (Multiplayer.Client == null) return; - if (WorldRendererUtility.WorldRenderedNow) return; + if (WorldRendererUtility.WorldRendered) return; var map = PreDrawCalcMarker.calculating.Map ?? Find.CurrentMap; var asyncTime = map.AsyncTime(); @@ -146,7 +146,7 @@ static class TickManagerPausedPatch static void Postfix(ref bool __result) { if (Multiplayer.Client == null) return; - if (WorldRendererUtility.WorldRenderedNow) return; + if (WorldRendererUtility.WorldRendered) return; var asyncTime = Find.CurrentMap.AsyncTime(); var timeSpeed = Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : asyncTime.DesiredTimeSpeed; diff --git a/Source/Client/AsyncTime/SetMapTime.cs b/Source/Client/AsyncTime/SetMapTime.cs index 91c706b7..a8257b1f 100644 --- a/Source/Client/AsyncTime/SetMapTime.cs +++ b/Source/Client/AsyncTime/SetMapTime.cs @@ -23,13 +23,13 @@ static IEnumerable TargetMethods() yield return AccessTools.Method(typeof(MapInterface), nameof(MapInterface.MapInterfaceUpdate)); yield return AccessTools.Method(typeof(AlertsReadout), nameof(AlertsReadout.AlertsReadoutUpdate)); yield return AccessTools.Method(typeof(SoundRoot), nameof(SoundRoot.Update)); - yield return AccessTools.Method(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.ChoicesAtFor)); + yield return AccessTools.Method(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.GetOptions)); } [HarmonyPriority(MpPriority.MpFirst)] internal static void Prefix(ref TimeSnapshot? __state) { - if (Multiplayer.Client == null || WorldRendererUtility.WorldRenderedNow || Find.CurrentMap == null) return; + if (Multiplayer.Client == null || WorldRendererUtility.WorldSelected || Find.CurrentMap == null) return; __state = TimeSnapshot.GetAndSetFromMap(Find.CurrentMap); } diff --git a/Source/Client/AsyncTime/TimeControlUI.cs b/Source/Client/AsyncTime/TimeControlUI.cs index c4edd005..ea923efa 100644 --- a/Source/Client/AsyncTime/TimeControlUI.cs +++ b/Source/Client/AsyncTime/TimeControlUI.cs @@ -21,7 +21,7 @@ public static class TimeControlPatch private static bool ShouldReset => Event.current.shift && Multiplayer.GameComp.IsLowestWins; private static ITickable Tickable => - !WorldRendererUtility.WorldRenderedNow && Multiplayer.GameComp.asyncTime + !WorldRendererUtility.WorldRendered && Multiplayer.GameComp.asyncTime ? Find.CurrentMap.AsyncTime() : Multiplayer.AsyncWorldTime; @@ -394,7 +394,7 @@ static void SwitchToMapOrWorld(Map map) } else { - if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld(); + if (WorldRendererUtility.WorldRendered) CameraJumper.TryHideWorld(); Current.Game.CurrentMap = map; } } diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index 08e84cb1..73bc977d 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -65,7 +65,7 @@ public static void SpawnCaravans() { Pawn item = PawnGenerator.GeneratePawn( DefDatabase.AllDefs - .Where((PawnKindDef d) => d.RaceProps.Animal && d.RaceProps.wildness < 1f).RandomElement(), + .Where((PawnKindDef d) => d.RaceProps.Animal && d.race.GetStatValueAbstract(StatDefOf.Wildness) < 1f).RandomElement(), Faction.OfPlayer); list.Add(item); } diff --git a/Source/Client/Debug/DebugSync.cs b/Source/Client/Debug/DebugSync.cs index 0515d756..ab4b0a59 100644 --- a/Source/Client/Debug/DebugSync.cs +++ b/Source/Client/Debug/DebugSync.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using HarmonyLib; using LudeonTK; +using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; +using static HarmonyLib.AccessTools; namespace Multiplayer.Client { @@ -43,7 +45,7 @@ public static void HandleCmd(ByteReader data) List prevWorldSelected = Find.WorldSelector.selected; Find.Selector.selected = new List(); - Find.WorldSelector.selected = new List(); + FieldRefs.worldSelected(Find.WorldSelector) = new List(); int selectedId = data.ReadInt32(); @@ -108,7 +110,7 @@ public static void HandleCmd(ByteReader data) MouseCellPatch.result = null; MouseTilePatch.result = null; Find.Selector.selected = prevSelected; - Find.WorldSelector.selected = prevWorldSelected; + FieldRefs.worldSelected(Find.WorldSelector) = prevWorldSelected; currentHash = 0; currentPlayer = -1; @@ -246,7 +248,7 @@ public void Action() DebugSource.Tree, 0, node.NodePath(), - WorldRendererUtility.WorldRenderedNow ? null : Find.CurrentMap + WorldRendererUtility.WorldRendered ? null : Find.CurrentMap ); } } diff --git a/Source/Client/Factions/Blueprints.cs b/Source/Client/Factions/Blueprints.cs index 55da6117..2b39e335 100644 --- a/Source/Client/Factions/Blueprints.cs +++ b/Source/Client/Factions/Blueprints.cs @@ -15,7 +15,7 @@ namespace Multiplayer.Client // Don't draw other factions' blueprints // Don't link graphics of different factions' blueprints - [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintAt_NewTemp))] + [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintAt))] static class CanPlaceBlueprintAtPatch { static MethodInfo CanPlaceBlueprintOver = AccessTools.Method(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintOver)); @@ -48,8 +48,7 @@ static IEnumerable Transpiler(IEnumerable e, M static bool ShouldIgnore1(Thing oldThing) => oldThing.def.IsBlueprint && oldThing.Faction != Faction.OfPlayer; } - - [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintAt_NewTemp))] + [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.CanPlaceBlueprintAt))] static class CanPlaceBlueprintAtPatch2 { static IEnumerable Transpiler(IEnumerable e, MethodBase original) @@ -63,39 +62,75 @@ static IEnumerable Transpiler(IEnumerable e, M List insts = e.ToList(); - int loop1 = new CodeFinder(original, insts). + int loop = new CodeFinder(original, insts). Forward(OpCodes.Ldstr, "IdenticalThingExists"). Backward(OpCodes.Ldarg_S, thingToIgnore_Ldarg_S); insts.Insert( - loop1 - 1, - new CodeInstruction(OpCodes.Ldloc_S, insts[loop1 - 1].operand), + loop - 1, + new CodeInstruction(OpCodes.Ldloc_S, insts[loop - 1].operand), new CodeInstruction(OpCodes.Call, CanPlaceBlueprintAtPatch.ShouldIgnore1Method), - new CodeInstruction(OpCodes.Brtrue, insts[loop1 + 1].operand) + new CodeInstruction(OpCodes.Brtrue, insts[loop + 1].operand) ); - int loop2 = new CodeFinder(original, insts). + return insts; + } + } + + [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.InteractionCellStandable))] + static class InteractionCellStandablePatch + { + static IEnumerable Transpiler(IEnumerable e, MethodBase original) + { + byte thingToIgnore_Ldarg_S = (byte) original.GetParameters().FirstIndexOf(p => p.Name == "thingToIgnore"); + + if (thingToIgnore_Ldarg_S < 1) { + Log.Error($"FAIL: {nameof(InteractionCellStandablePatch)} can't find thingToIgnore"); + return e; + } + + List insts = e.ToList(); + + int loop = new CodeFinder(original, insts). Forward(OpCodes.Ldstr, "InteractionSpotBlocked"). Backward(OpCodes.Ldarg_S, thingToIgnore_Ldarg_S); insts.Insert( - loop2 - 3, - new CodeInstruction(OpCodes.Ldloc_S, insts[loop2 - 3].operand), - new CodeInstruction(OpCodes.Ldloc_S, insts[loop2 - 2].operand), + loop - 3, + new CodeInstruction(insts[loop - 3].opcode, insts[loop - 3].operand), + new CodeInstruction(insts[loop - 2].opcode, insts[loop - 2].operand), new CodeInstruction(OpCodes.Callvirt, SpawnBuildingAsPossiblePatch.ThingListGet), new CodeInstruction(OpCodes.Call, CanPlaceBlueprintAtPatch.ShouldIgnore1Method), - new CodeInstruction(OpCodes.Brtrue, insts[loop2 + 1].operand) + new CodeInstruction(OpCodes.Brtrue, insts[loop + 1].operand) ); + + return insts; + } + } + + [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.NotBlockingAnyInteractionCells))] + static class NotBlockingAnyInteractionCellsPatch + { + static IEnumerable Transpiler(IEnumerable e, MethodBase original) + { + byte thingToIgnore_Ldarg_S = (byte) original.GetParameters().FirstIndexOf(p => p.Name == "thingToIgnore"); + + if (thingToIgnore_Ldarg_S < 1) { + Log.Error($"FAIL: {nameof(NotBlockingAnyInteractionCellsPatch)} can't find thingToIgnore"); + return e; + } + + List insts = e.ToList(); - int loop3 = new CodeFinder(original, insts). + int loop = new CodeFinder(original, insts). Forward(OpCodes.Ldstr, "WouldBlockInteractionSpot"). Backward(OpCodes.Ldarg_S, thingToIgnore_Ldarg_S); insts.Insert( - loop3 - 1, - new CodeInstruction(OpCodes.Ldloc_S, insts[loop3 - 1].operand), + loop - 1, + new CodeInstruction(insts[loop - 1].opcode, insts[loop - 1].operand), new CodeInstruction(OpCodes.Call, CanPlaceBlueprintAtPatch.ShouldIgnore1Method), - new CodeInstruction(OpCodes.Brtrue, insts[loop3 + 1].operand) + new CodeInstruction(OpCodes.Brtrue, insts[loop + 1].operand) ); return insts; diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs index 55036296..9c5a65ac 100644 --- a/Source/Client/Factions/FactionContextSetters.cs +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using HarmonyLib; using RimWorld; using RimWorld.Planet; @@ -20,10 +21,11 @@ static void Finalizer() } } -[HarmonyPatch(typeof(GetOrGenerateMapUtility), nameof(GetOrGenerateMapUtility.GetOrGenerateMap), new []{ typeof(int), typeof(IntVec3), typeof(WorldObjectDef) })] +[HarmonyPatch(typeof(GetOrGenerateMapUtility), nameof(GetOrGenerateMapUtility.GetOrGenerateMap), typeof(PlanetTile), typeof(IntVec3), typeof(WorldObjectDef), + typeof(IEnumerable), typeof(bool))] static class MapGenFactionPatch { - static void Prefix(int tile) + static void Prefix(PlanetTile tile) { var mapParent = Find.WorldObjects.MapParentAt(tile); if (Multiplayer.Client != null && mapParent == null) diff --git a/Source/Client/Factions/FactionCreator.cs b/Source/Client/Factions/FactionCreator.cs index 12e5d62b..f83315e4 100644 --- a/Source/Client/Factions/FactionCreator.cs +++ b/Source/Client/Factions/FactionCreator.cs @@ -28,7 +28,7 @@ public static void SendPawn(int playerId, Pawn p) [SyncMethod] public static void CreateFaction( - int playerId, string factionName, int tile, + int playerId, string factionName, PlanetTile tile, [CanBeNull] ScenarioDef scenarioDef, ChooseIdeoInfo chooseIdeoInfo, bool generateMap ) @@ -80,7 +80,7 @@ bool generateMap }, "GeneratingMap", doAsynchronously: true, GameAndMapInitExceptionHandlers.ErrorWhileGeneratingMap); } - private static Map GenerateNewMap(int tile, Scenario scenario) + private static Map GenerateNewMap(PlanetTile tile, Scenario scenario) { // This has to be null, otherwise, during map generation, Faction.OfPlayer returns it which breaks FactionContext Find.GameInitData.playerFaction = null; diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 7aae71ad..96e580b8 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -193,7 +193,7 @@ static bool FactionIsPlayer(Faction f) [HarmonyPatch(typeof(Ideo), nameof(Ideo.RecacheColonistBelieverCount))] static class RecacheColonistBelieverCountPatch { - private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravelingTransportPods_Alive_FreeColonists_NoCryptosleep)); + private static MethodInfo allColonists = AccessTools.PropertyGetter(typeof(PawnsFinder), nameof(PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive_FreeColonists_NoCryptosleep)); static IEnumerable Transpiler(IEnumerable insts) { @@ -211,7 +211,7 @@ private static List ColonistsAllPlayerFactions() { colonistsAllFactions.Clear(); - foreach (var p in PawnsFinder.AllMapsCaravansAndTravelingTransportPods_Alive) + foreach (var p in PawnsFinder.AllMapsCaravansAndTravellingTransporters_Alive) { if (IsColonistAnyFaction(p) && p.HostFaction == null && !p.InCryptosleep) colonistsAllFactions.Add(p); @@ -286,7 +286,7 @@ static void Postfix(MapPawns __instance, ref bool __result) static class IsValidColonyPawnPatch { private static MethodInfo isColonist = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonist)); - private static MethodInfo isColonyMutant = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonyMutant)); + private static MethodInfo isColonySubhuman = AccessTools.PropertyGetter(typeof(Pawn), nameof(Pawn.IsColonySubhuman)); static IEnumerable Transpiler(IEnumerable insts) { @@ -295,7 +295,7 @@ static IEnumerable Transpiler(IEnumerable inst if (inst.operand == isColonist) inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); - if (inst.operand == isColonyMutant) + if (inst.operand == isColonySubhuman) inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMutantAnyFaction)); yield return inst; diff --git a/Source/Client/Patches/AllGroupsPatch.cs b/Source/Client/Patches/AllGroupsPatch.cs index 9b7884e5..ca12be88 100644 --- a/Source/Client/Patches/AllGroupsPatch.cs +++ b/Source/Client/Patches/AllGroupsPatch.cs @@ -21,7 +21,7 @@ static IEnumerable Transpiler(IEnumerable inst { if (inst.operand == AllGroups) { - yield return new CodeInstruction(OpCodes.Ldarg_1); + yield return new CodeInstruction(OpCodes.Ldarg_1).MoveLabelsFrom(inst); yield return new CodeInstruction(OpCodes.Call, method); } else diff --git a/Source/Client/Patches/ArbiterPatches.cs b/Source/Client/Patches/ArbiterPatches.cs index 328ae75e..4cd05545 100644 --- a/Source/Client/Patches/ArbiterPatches.cs +++ b/Source/Client/Patches/ArbiterPatches.cs @@ -71,25 +71,27 @@ static IEnumerable TargetMethods() static bool Prefix() => !Multiplayer.arbiterInstance; } - [HarmonyPatch(typeof(WorldRenderer), MethodType.Constructor)] - static class CancelWorldRendererCtor + // TODO test if this works. + [HarmonyPatch(typeof(WorldGrid), (nameof(WorldGrid.InitializeGlobalLayers)))] + static class NoWorldRenderLayersForArbiter { static bool Prefix() => !Multiplayer.arbiterInstance; - static void Postfix(WorldRenderer __instance) + static void Postfix(WorldGrid __instance) { if (Multiplayer.arbiterInstance) - __instance.layers = new List(); + __instance.globalLayers.Clear(); } } - [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.LongEventsUpdate))] - static class ArbiterLongEventPatch + // TODO: Test if it works in 1.6, we may need a way to fully prevent the layers from being initialized + [HarmonyPatch(typeof(PlanetLayer), nameof(PlanetLayer.InitializeLayer))] + static class NoPlanetRenderLayersForArbiter { - static void Postfix() + static void Postfix(PlanetLayer __instance) { - if (Multiplayer.arbiterInstance && LongEventHandler.currentEvent != null) - LongEventHandler.currentEvent.alreadyDisplayed = true; + if (Multiplayer.arbiterInstance) + __instance.WorldDrawLayers.Clear(); } } } diff --git a/Source/Client/Patches/HashCodes.cs b/Source/Client/Patches/HashCodes.cs index 3fb13123..9daa7bca 100644 --- a/Source/Client/Patches/HashCodes.cs +++ b/Source/Client/Patches/HashCodes.cs @@ -8,12 +8,15 @@ namespace Multiplayer.Client.Patches { - [HarmonyPatch(typeof(GlowGrid), MethodType.Constructor, new[] { typeof(Map) })] + [HarmonyPatch(typeof(GlowGrid), MethodType.Constructor, typeof(Map))] static class GlowGridCtorPatch { + private static AccessTools.FieldRef> litGlowers = + AccessTools.FieldRefAccess>(nameof(GlowGrid.litGlowers)); + static void Postfix(GlowGrid __instance) { - __instance.litGlowers = new HashSet(new CompGlowerEquality()); + litGlowers(__instance) = new HashSet(new CompGlowerEquality()); } class CompGlowerEquality : IEqualityComparer diff --git a/Source/Client/Patches/LongEvents.cs b/Source/Client/Patches/LongEvents.cs index b07b4e40..0f6d92d2 100644 --- a/Source/Client/Patches/LongEvents.cs +++ b/Source/Client/Patches/LongEvents.cs @@ -7,7 +7,8 @@ namespace Multiplayer.Client.Patches { - [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action))] + [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), + typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(bool), typeof(Action))] static class MarkLongEvents { private static MethodInfo MarkerMethod = AccessTools.Method(typeof(MarkLongEvents), nameof(Marker)); @@ -62,7 +63,8 @@ static void Postfix() } } - [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), new[] { typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action) })] + [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), + typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(bool), typeof(Action))] static class LongEventAlwaysSync { static void Prefix(ref bool doAsynchronously) diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 37e4e25c..7cfe8de8 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -183,9 +183,9 @@ static void Postfix(ref IntVec3 __result) [HarmonyPatch(typeof(GenWorld), nameof(GenWorld.MouseTile))] public static class MouseTilePatch { - public static int? result; + public static PlanetTile? result; - static void Postfix(ref int __result) + static void Postfix(ref PlanetTile __result) { if (result.HasValue) __result = result.Value; @@ -246,7 +246,8 @@ static class RootPlayStartMarker static void Finalizer() => starting = false; } - [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), new[] { typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action) })] + [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), + typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(bool), typeof(Action))] static class CancelRootPlayStartLongEvents { public static bool cancel; @@ -357,7 +358,7 @@ static void Postfix(WorldRoutePlanner __instance, ref bool __result) if (Multiplayer.Client == null) return; // Ignore unpausing - if (__result && __instance.active && WorldRendererUtility.WorldRenderedNow) + if (__result && __instance.active && WorldRendererUtility.WorldSelected) __result = false; } } diff --git a/Source/Client/Patches/Seeds.cs b/Source/Client/Patches/Seeds.cs index 9aa771a5..0a0da5f2 100644 --- a/Source/Client/Patches/Seeds.cs +++ b/Source/Client/Patches/Seeds.cs @@ -91,7 +91,8 @@ static void Postfix(Map map, bool __state) } } - [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), typeof(Action), typeof(string), typeof(bool), typeof(Action), typeof(bool), typeof(Action))] + [HarmonyPatch(typeof(LongEventHandler), nameof(LongEventHandler.QueueLongEvent), typeof(Action), typeof(string), typeof(bool), typeof(Action), + typeof(bool), typeof(bool), typeof(Action))] static class SeedLongEvents { static void Prefix(ref Action action) diff --git a/Source/Client/Patches/ThingMethodPatches.cs b/Source/Client/Patches/ThingMethodPatches.cs index a3457f68..8ce42dbc 100644 --- a/Source/Client/Patches/ThingMethodPatches.cs +++ b/Source/Client/Patches/ThingMethodPatches.cs @@ -92,7 +92,7 @@ static void Finalizer(Container? __state) } } - [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.CheckForJobOverride_NewTemp))] + [HarmonyPatch(typeof(Pawn_JobTracker), nameof(Pawn_JobTracker.CheckForJobOverride))] public static class JobTrackerOverride { static void Prefix(Pawn_JobTracker __instance, ref Container? __state) diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index f3740f8a..e1fce8db 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -148,7 +148,7 @@ public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action static ITickable CurrentTickable() { - if (WorldRendererUtility.WorldRenderedNow) + if (WorldRendererUtility.WorldRendered) return Multiplayer.AsyncWorldTime; if (Find.CurrentMap != null) diff --git a/Source/Client/Patches/TileTemperatures.cs b/Source/Client/Patches/TileTemperatures.cs index 328f0882..02b8e3e3 100644 --- a/Source/Client/Patches/TileTemperatures.cs +++ b/Source/Client/Patches/TileTemperatures.cs @@ -12,7 +12,7 @@ namespace Multiplayer.Client [HarmonyPatch(nameof(TileTemperaturesComp.CachedTileTemperatureData.CheckCache))] static class CachedTileTemperatureData_CheckCache { - static void Prefix(int ___tile, ref TimeSnapshot? __state) + static void Prefix(PlanetTile ___tile, ref TimeSnapshot? __state) { if (Multiplayer.Client == null) return; @@ -28,7 +28,7 @@ static void Prefix(int ___tile, ref TimeSnapshot? __state) [HarmonyPatch(typeof(TileTemperaturesComp), nameof(TileTemperaturesComp.RetrieveCachedData))] static class RetrieveCachedData_Patch { - static bool Prefix(TileTemperaturesComp __instance, int tile, ref TileTemperaturesComp.CachedTileTemperatureData __result) + static bool Prefix(TileTemperaturesComp __instance, PlanetTile tile, ref TileTemperaturesComp.CachedTileTemperatureData __result) { if (Multiplayer.InInterface && __instance != Multiplayer.WorldComp.uiTemperatures) { @@ -53,14 +53,14 @@ static void Prefix(TileTemperaturesComp __instance) [HarmonyPatch(typeof(GenTemperature), nameof(GenTemperature.AverageTemperatureAtTileForTwelfth))] static class CacheAverageTileTemperature { - static Dictionary averageTileTemps = new Dictionary(); + static Dictionary averageTileTemps = new Dictionary(); - static bool Prefix(int tile, Twelfth twelfth) + static bool Prefix(PlanetTile tile, Twelfth twelfth) { return !averageTileTemps.TryGetValue(tile, out float[] arr) || float.IsNaN(arr[(int)twelfth]); } - static void Postfix(int tile, Twelfth twelfth, ref float __result) + static void Postfix(PlanetTile tile, Twelfth twelfth, ref float __result) { if (averageTileTemps.TryGetValue(tile, out float[] arr) && !float.IsNaN(arr[(int)twelfth])) { @@ -85,8 +85,8 @@ static class ClearTemperatureCache { static IEnumerable TargetMethods() { - yield return AccessTools.Method(typeof(WorldGrid), nameof(WorldGrid.RawDataToTiles)); - yield return AccessTools.Method(typeof(WorldGenStep_Terrain), nameof(WorldGenStep_Terrain.GenerateGridIntoWorld)); + yield return AccessTools.Method(typeof(PlanetLayer), nameof(PlanetLayer.RawDataToTiles)); + yield return AccessTools.Method(typeof(WorldGenStep_Terrain), nameof(WorldGenStep_Terrain.GenerateFresh)); } static void Postfix() => CacheAverageTileTemperature.Clear(); diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index 9dab7444..4f5ef3c5 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -132,7 +132,7 @@ static bool Prefix(Dialog_FormCaravan __instance) [HarmonyPatch(typeof(Dialog_FormCaravan), nameof(Dialog_FormCaravan.Notify_ChoseRoute))] static class Notify_ChoseRoutePatch { - static bool Prefix(Dialog_FormCaravan __instance, int destinationTile) + static bool Prefix(Dialog_FormCaravan __instance, PlanetTile destinationTile) { if (Multiplayer.InInterface && __instance is CaravanFormingProxy dialog) { @@ -168,7 +168,7 @@ static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action o if (__instance.GetType() != typeof(Dialog_FormCaravan)) return; - // Handles showing the dialog from TimedForcedExit.CompTick -> TimedForcedExit.ForceReform + // Handles showing the dialog from TimedForcedExit.CompTickInterval -> TimedForcedExit.ForceReform // (note TimedForcedExit is obsolete) if (Multiplayer.ExecutingCmds || Multiplayer.Ticking) { @@ -215,10 +215,10 @@ internal static void StartFormingCaravan(Map map, bool reform = false, IntVec3? } } - [HarmonyPatch(typeof(FormCaravanGizmoUtility), nameof(FormCaravanGizmoUtility.DialogFromToSettlement))] + [HarmonyPatch(typeof(WorldGizmoUtility), nameof(WorldGizmoUtility.DialogFromToSettlement))] static class HandleFormCaravanShowRoutePlanner { - static bool Prefix(Map origin, int tile) + static bool Prefix(Map origin, PlanetTile tile) { if (Multiplayer.Client == null) return true; @@ -230,7 +230,7 @@ static bool Prefix(Map origin, int tile) } } - [HarmonyPatch(typeof(TimedForcedExit), nameof(TimedForcedExit.CompTick))] + [HarmonyPatch(typeof(TimedForcedExit), nameof(TimedForcedExit.CompTickInterval))] static class TimedForcedExitTickPatch { static bool Prefix(TimedForcedExit __instance) diff --git a/Source/Client/Persistent/Trading.cs b/Source/Client/Persistent/Trading.cs index 1092848c..a67660b8 100644 --- a/Source/Client/Persistent/Trading.cs +++ b/Source/Client/Persistent/Trading.cs @@ -752,7 +752,7 @@ static void Postfix(Pawn_AgeTracker __instance) } } - [HarmonyPatch(typeof(Pawn_AgeTracker), nameof(Pawn_AgeTracker.AgeTick))] + [HarmonyPatch(typeof(Pawn_AgeTracker), nameof(Pawn_AgeTracker.AgeTickInterval))] static class PawnAgeChanged { static void Prefix(Pawn_AgeTracker __instance, ref int __state) diff --git a/Source/Client/Saving/CacheForReloading.cs b/Source/Client/Saving/CacheForReloading.cs index 483573d1..5e14b86d 100644 --- a/Source/Client/Saving/CacheForReloading.cs +++ b/Source/Client/Saving/CacheForReloading.cs @@ -2,12 +2,14 @@ using RimWorld.Planet; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Verse; +// TODO: TEST: Test that this works with the new world generation + namespace Multiplayer.Client { - [HarmonyPatch(typeof(MapDrawer), nameof(MapDrawer.RegenerateEverythingNow))] public static class MapDrawerRegenPatch { @@ -67,15 +69,15 @@ static bool Prefix(WorldGrid __instance, ref int ___cachedTraversalDistance, ref WorldGrid grid = __instance; - grid.viewAngle = copyFrom.viewAngle; - grid.viewCenter = copyFrom.viewCenter; - grid.verts = copyFrom.verts; - grid.tileIDToNeighbors_offsets = copyFrom.tileIDToNeighbors_offsets; - grid.tileIDToNeighbors_values = copyFrom.tileIDToNeighbors_values; - grid.tileIDToVerts_offsets = copyFrom.tileIDToVerts_offsets; - grid.averageTileSize = copyFrom.averageTileSize; + grid.surfaceViewAngle = copyFrom.SurfaceViewAngle; + grid.surfaceViewCenter = copyFrom.SurfaceViewCenter; + grid.surface.verts = copyFrom.UnsafeVerts; + grid.surface.tileIDToNeighbors_offsets = copyFrom.UnsafeTileIDToNeighbors_offsets; + grid.surface.tileIDToNeighbors_values = copyFrom.UnsafeTileIDToNeighbors_values; + grid.surface.tileIDToVerts_offsets = copyFrom.UnsafeTileIDToVerts_offsets; + grid.surface.averageTileSize = copyFrom.AverageTileSize; + grid.surface.tiles.Clear(); - grid.tiles = new List(); ___cachedTraversalDistance = -1; ___cachedTraversalDistanceForStart = -1; ___cachedTraversalDistanceForEnd = -1; @@ -97,23 +99,39 @@ static bool Prefix(WorldGrid __instance) WorldGrid grid = __instance; - grid.tileBiome = copyFrom.tileBiome; - grid.tileElevation = copyFrom.tileElevation; - grid.tileHilliness = copyFrom.tileHilliness; - grid.tileTemperature = copyFrom.tileTemperature; - grid.tileRainfall = copyFrom.tileRainfall; - grid.tileSwampiness = copyFrom.tileSwampiness; - grid.tileFeature = copyFrom.tileFeature; - grid.tileRoadOrigins = copyFrom.tileRoadOrigins; - grid.tileRoadAdjacency = copyFrom.tileRoadAdjacency; - grid.tileRoadDef = copyFrom.tileRoadDef; - grid.tileRiverOrigins = copyFrom.tileRiverOrigins; - grid.tileRiverAdjacency = copyFrom.tileRiverAdjacency; - grid.tileRiverDef = copyFrom.tileRiverDef; + List copyTiles = copyFrom.Tiles.ToList(); + List gridTiles = grid.Tiles.ToList(); + + for(int i = 0; i < copyTiles.Count; i++) + { + SurfaceTile sourceTile = copyTiles[i]; + SurfaceTile targetTile = gridTiles[i]; + + // Tile + targetTile.biome = sourceTile.biome; + targetTile.elevation = sourceTile.elevation; + targetTile.hilliness = sourceTile.hilliness; + targetTile.temperature = sourceTile.temperature; + targetTile.rainfall = sourceTile.rainfall; + targetTile.swampiness = sourceTile.swampiness; + targetTile.feature = sourceTile.feature; + targetTile.pollution = sourceTile.pollution; + targetTile.tile = sourceTile.tile; + targetTile.mutatorsNullable = sourceTile.mutatorsNullable; + + // Surface Tile - Roads/Rivers are getters for potentialRoads/potentialRivers + targetTile.potentialRoads = sourceTile.potentialRoads; + targetTile.riverDist = sourceTile.riverDist; + targetTile.potentialRivers = sourceTile.potentialRivers; + } // This is plain old data apart from the WorldFeature feature field which is a reference // It later gets reset in WorldFeatures.ExposeData though so it can be safely copied - grid.tiles = copyFrom.tiles; + + // Use Clear/AddRange instead of reflection to preserve collection observers + // and handle readonly field correctly + grid.surface.tiles.Clear(); + grid.surface.tiles.AddRange(copyFrom.surface.tiles); // ExposeData runs multiple times but WorldGrid only needs LoadSaveMode.LoadingVars copyFrom = null; @@ -122,19 +140,6 @@ static bool Prefix(WorldGrid __instance) } } - [HarmonyPatch(typeof(WorldRenderer), MethodType.Constructor)] - public static class WorldRendererCachePatch - { - public static WorldRenderer copyFrom; - - static bool Prefix(WorldRenderer __instance) - { - if (copyFrom == null) return true; - - __instance.layers = copyFrom.layers; - copyFrom = null; - - return false; - } - } + // WorldRenderer patch removed since AllDrawLayers is computed dynamically + // and there's no actual caching being performed } diff --git a/Source/Client/Saving/SaveLoad.cs b/Source/Client/Saving/SaveLoad.cs index bfb76b1a..1390bc42 100644 --- a/Source/Client/Saving/SaveLoad.cs +++ b/Source/Client/Saving/SaveLoad.cs @@ -57,10 +57,11 @@ public static TempGameData SaveAndReload() gameData = SaveGameData(); } - MapDrawerRegenPatch.copyFrom = drawers; - WorldGridCachePatch.copyFrom = worldGridSaved; - WorldGridExposeDataPatch.copyFrom = worldGridSaved; - WorldRendererCachePatch.copyFrom = worldRendererSaved; + // TODO + //MapDrawerRegenPatch.copyFrom = drawers; + //WorldGridCachePatch.copyFrom = worldGridSaved; + //WorldGridExposeDataPatch.copyFrom = worldGridSaved; + //WorldRendererCachePatch.copyFrom = worldRendererSaved; MusicManagerPlay musicManager = null; if (Find.MusicManagerPlay.gameObjectCreated) diff --git a/Source/Client/Syncing/Dict/SyncDictMisc.cs b/Source/Client/Syncing/Dict/SyncDictMisc.cs index ef02dc04..354f4395 100644 --- a/Source/Client/Syncing/Dict/SyncDictMisc.cs +++ b/Source/Client/Syncing/Dict/SyncDictMisc.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using HarmonyLib; using Multiplayer.API; using Multiplayer.Common; @@ -84,6 +86,34 @@ public static class SyncDictMisc worker.Bind(ref color.a); } }, + { + // TODO: Check if this will actually be needed, remove if not + (ByteWriter data, FloatMenuContext context) => + { + data.MpContext().map = context.map; + + WriteSync(data, context.allSelectedPawns); + WriteSync(data, context.clickPosition); + WriteSync(data, context.cachedClickedCell); + WriteSync(data, context.cachedClickedThings); + WriteSync(data, context.cachedClickedRoom); + WriteSync(data, context.cachedClickedZone); + }, + (ByteReader reader) => + { + var context = new FloatMenuContext(ReadSync>(reader), ReadSync(reader), reader.MpContext().map) + { + cachedClickedCell = ReadSync(reader), + cachedClickedThings = ReadSync>(reader), + cachedClickedRoom = ReadSync(reader), + cachedClickedZone = ReadSync(reader) + }; + + context.cachedClickedPawns = context.cachedClickedThings.OfType().ToList(); + + return context; + } + }, #endregion #region Unity diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index 0ceefbe2..df4507ee 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -881,6 +881,28 @@ public static class SyncDictRimWorld return Find.World.worldObjects.AllWorldObjects.Find(w => w.ID == objId); }, true // Implicit }, + { + (ByteWriter data, Action action) => + { + WriteSync(data, action.Target); + data.WriteString(action.Method.Name); + }, + (ByteReader data) => + { + // ReadSyncObject can infer the type from the data stream. + var target = ReadSync(data); + var methodName = data.ReadString(); + // Use the ReadSync helper for arrays. + var parameterTypes = ReadSync(data); + + if (target == null || methodName == null) return null; + + var method = AccessTools.Method(target.GetType(), methodName, parameterTypes); + if (method == null) return null; + + return (Action)Delegate.CreateDelegate(typeof(Action), target, method); + } + }, { (SyncWorker data, ref WorldObjectComp comp) => { if (data.isWriting) { @@ -933,6 +955,16 @@ public static class SyncDictRimWorld (ByteWriter data, Caravan_ForageTracker tracker) => WriteSync(data, tracker?.caravan), (ByteReader data) => ReadSync(data)?.forage }, + { + // TODO: Consider using int16 rather that int32 to minimize network traffic. + // Investigate if the tiles/layers are small enough to allow that. + (ByteWriter data, PlanetTile tile) => + { + data.WriteInt32(tile.tileId); + data.WriteInt32(tile.layerId); + }, + (ByteReader data) => new PlanetTile(data.ReadInt32(), data.ReadInt32()), true // Implicit + }, #endregion #region Game diff --git a/Source/Client/Syncing/Game/SyncActions.cs b/Source/Client/Syncing/Game/SyncActions.cs index 5d029b4b..dcfa71f5 100644 --- a/Source/Client/Syncing/Game/SyncActions.cs +++ b/Source/Client/Syncing/Game/SyncActions.cs @@ -1,4 +1,4 @@ -using RimWorld; +using RimWorld; using RimWorld.Planet; using System; using System.Collections.Generic; @@ -10,15 +10,15 @@ namespace Multiplayer.Client static class SyncActions { static SyncAction SyncWorldObjCaravanMenus; - static SyncAction, CompLaunchable> SyncTransportPodMenus; + static SyncAction, Action> SyncTransportPodMenus; public static void Init() { SyncWorldObjCaravanMenus = RegisterActions((WorldObject obj, Caravan c) => obj.GetFloatMenuOptions(c), o => ref o.action); SyncWorldObjCaravanMenus.PatchAll(nameof(WorldObject.GetFloatMenuOptions)); - SyncTransportPodMenus = RegisterActions((WorldObject obj, IEnumerable p, CompLaunchable r) => obj.GetTransportPodsFloatMenuOptions(p, r), o => ref o.action); - SyncTransportPodMenus.PatchAll(nameof(WorldObject.GetTransportPodsFloatMenuOptions)); + SyncTransportPodMenus = RegisterActions((WorldObject obj, IEnumerable p, Action a) => obj.GetTransportersFloatMenuOptions(p, a), o => ref o.action); + SyncTransportPodMenus.PatchAll(nameof(WorldObject.GetTransportersFloatMenuOptions)); } static SyncAction RegisterActions(Func> func, ActionGetter actionGetter) diff --git a/Source/Client/Syncing/Game/SyncDelegates.cs b/Source/Client/Syncing/Game/SyncDelegates.cs index caf07fa3..67914907 100644 --- a/Source/Client/Syncing/Game/SyncDelegates.cs +++ b/Source/Client/Syncing/Game/SyncDelegates.cs @@ -18,20 +18,56 @@ public static void Init() { const SyncContext mouseKeyContext = SyncContext.QueueOrder_Down | SyncContext.MapMouseCell; - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.GotoLocationOption), 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Goto - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Arrest - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 6).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Rescue - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 7).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Capture slave - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 8).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Capture prisoner - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 9).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry to cryptosleep casket - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 12).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Capture entity - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), 50).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Reload - SyncDelegate.LocalFunc(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddHumanlikeOrders), "CarryToShuttleAct").CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry to shuttle - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddDraftedOrders), 3).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted carry to bed - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddDraftedOrders), 4).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted carry to bed (arrest) - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddDraftedOrders), 5).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted carry entity to holding building - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddDraftedOrders), 6).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted carry to transport shuttle - SyncDelegate.Lambda(typeof(FloatMenuMakerMap), nameof(FloatMenuMakerMap.AddDraftedOrders), 7).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted carry to cryptosleep casket + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Arrest), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Arrest + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_BringBabyToSafety), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Bring baby to safety + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CaptureEntity), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Capture entity + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CapturePawn), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Capture pawn (prisoner or slave) + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryDeathrestingToCasket), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry deathresting to casket + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryMechToCharger), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry mech to charger + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryPawnToExit), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry pawn to exit grid + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryToBiosculpterPod), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry to biosculpter pod + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryToCryptosleepCasket), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry to cryptosleep casket + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CleanRoom), "GetSingleOption", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Clean room + SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Deathrest), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Deathrest + + //SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryingPawn), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry downed pawn + //SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_CarryToShuttle), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Carry to shuttle + //SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Childcare), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Breastfeed/give milk + //SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DraftedAttack), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted attack + //SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DraftedMove), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted move (Goto) + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DraftedRepair), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted repair + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DraftedTend), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drafted tend + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DressOtherPawn), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Dress other pawn + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_DropEquipment), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Drop equipment + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_EnterMapPortal), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Enter map portal + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Equip), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Equip + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_ExtinguishFires), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Extinguish fires + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_GhoulRest), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Ghoul rest + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_HackAncientTerminal), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Hack ancient terminal + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_HandleCorpse), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Handle corpse (bury/etc) + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Ingest), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Ingest + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_InvokeArchotech), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Invoke archotech + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_LoadCaravan), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Load caravan + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_LoadOntoPackAnimal), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Load into pack animal + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Mechanitor), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Mechanitor commands + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_OfferHelp), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Offer help + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_OpenThing), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Open + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_PickUpItem), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Pick up + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_PrisonerBloodfeed), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Prisoner bloodfeed + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_PutOutFireOnPawn), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Put out fire on pawn + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Relic), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Extract relic + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Reload), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Reload + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_RemoveMechlink), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Remove mechlink + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_RescuePawn), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Rescue pawn + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_ReturnSlaveToBed), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Return slave to bed + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Romance), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Romance attempt + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_StartRitual), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Start ritual + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Strip), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Strip + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Trade), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Trade + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_TransferEntity), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Transfer entity + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Wear), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Wear + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_WorkGivers), "GetOptionsFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Generic work givers + // SyncDelegate.Lambda(typeof(FloatMenuOptionProvider_Xenogerm), "GetSingleOptionFor", 0).CancelIfAnyFieldNull().SetContext(mouseKeyContext); // Implant xenogerm SyncDelegate.Lambda(typeof(Command_SetPlantToGrow), nameof(Command_SetPlantToGrow.ProcessInput), 2); // Set plant to grow SyncDelegate.Lambda(typeof(Building_Bed), nameof(Building_Bed.SetBedOwnerTypeByInterface), 0).RemoveNullsFromLists("bedsToAffect"); // Set bed owner type @@ -55,7 +91,7 @@ public static void Init() 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).SetContext(SyncContext.CurrentMap); // Remove all designations + SyncMethod.LambdaInGetter(typeof(Designator), nameof(Designator.RightClickFloatMenuOptions), 1).SetContext(SyncContext.CurrentMap); // Remove all designations SyncDelegate.Lambda(typeof(CaravanAbandonOrBanishUtility), nameof(CaravanAbandonOrBanishUtility.TryAbandonOrBanishViaInterface), 1, [typeof(Thing), typeof(Caravan)]).CancelIfAnyFieldNull(); // Abandon caravan thing SyncDelegate.Lambda(typeof(CaravanAbandonOrBanishUtility), nameof(CaravanAbandonOrBanishUtility.TryAbandonOrBanishViaInterface), 0, [typeof(TransferableImmutable), typeof(Caravan)]).CancelIfAnyFieldNull(); // Abandon caravan transferable @@ -156,8 +192,8 @@ public static void Init() // Disable 'needs overseer' effect/Allow mech undrafted orders are static fields that are remembered when joining, // could cause issues if someone pressed one of them and then started playing MP. Values reset on game restart. SyncMethod.Register(typeof(CompOverseerSubject), nameof(CompOverseerSubject.ForceFeral)).SetDebugOnly(); // Make feral - SyncMethod.Lambda(typeof(CompOverseerSubject), nameof(CompOverseerSubject.CompGetGizmosExtra), 4).SetDebugOnly(); // Make feral (event) - SyncDelegate.Lambda(typeof(CompOverseerSubject), nameof(CompOverseerSubject.CompGetGizmosExtra), 6).SetDebugOnly(); // Assign to overseer + SyncMethod.Register(typeof(CompOverseerSubject), nameof(CompOverseerSubject.TryMakeFeral)).SetDebugOnly(); // Make feral (event) + SyncDelegate.Lambda(typeof(CompOverseerSubject), nameof(CompOverseerSubject.CompGetGizmosExtra), 2).SetDebugOnly(); // Assign to overseer // Glower SyncMethod.Register(typeof(CompGlower), nameof(CompGlower.SetGlowColorInternal)); // Set color gizmo - will send a separate command per selected glower. Could be fixed with a transpiler for Dialog_GlowerColorPicker @@ -222,7 +258,7 @@ public static void Init() SyncMethod.Lambda(typeof(CompFleshmassSpitter), nameof(CompFleshmassSpitter.CompGetGizmosExtra), 0).SetDebugOnly(); // Remove spit cooldown // Dev mode gizmos - SyncDelegate.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 17).SetDebugOnly(); // Trigger random dissolution event (CompDissolution) + SyncDelegate.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 18).SetDebugOnly(); // Trigger random dissolution event (CompDissolution) SyncDelegate.Lambda(typeof(GroundSpawner), nameof(GroundSpawner.GetGizmos), 1).SetDebugOnly(); // Set spawn delay SyncDelegate.Lambda(typeof(GeneResourceDrainUtility), nameof(GeneResourceDrainUtility.GetResourceDrainGizmos), 0).SetDebugOnly(); // -10% resource SyncDelegate.Lambda(typeof(GeneResourceDrainUtility), nameof(GeneResourceDrainUtility.GetResourceDrainGizmos), 1).SetDebugOnly(); // +10% resource @@ -371,7 +407,7 @@ static void SetBabyName(ChoiceLetter_BabyBirth letter) } // If the baby ended up being stillborn, the timer to name them is 1 tick. This patch is here to allow players in MP to actually change their name. - [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome_NewTemp))] + [MpPostfix(typeof(PregnancyUtility), nameof(PregnancyUtility.ApplyBirthOutcome))] static void GiveTimeToNameStillborn(Thing __result) { if (Multiplayer.Client != null && __result is Pawn pawn && pawn.health.hediffSet.HasHediff(HediffDefOf.Stillborn)) diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index dc63361b..d38190c7 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -105,7 +105,7 @@ public static void Init() SyncMethod.Register(typeof(Building_SunLamp), nameof(Building_SunLamp.MakeMatchingGrowZone)); SyncMethod.Register(typeof(Building_ShipComputerCore), nameof(Building_ShipComputerCore.TryLaunch)); SyncMethod.Register(typeof(CompPower), nameof(CompPower.TryManualReconnect)); - SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature_NewTemp)); + SyncMethod.Register(typeof(CompTempControl), nameof(CompTempControl.InterfaceChangeTargetTemperature)); SyncMethod.Register(typeof(CompTransporter), nameof(CompTransporter.CancelLoad), Array.Empty()); SyncMethod.Register(typeof(MapPortal), nameof(MapPortal.CancelLoad)); SyncMethod.Register(typeof(StorageSettings), nameof(StorageSettings.CopyFrom)).ExposeParameter(0); @@ -122,7 +122,7 @@ public static void Init() SyncMethod.Register(typeof(SettlementAbandonUtility), nameof(SettlementAbandonUtility.Abandon)).CancelIfAnyArgNull(); SyncMethod.Register(typeof(WorldSelector), nameof(WorldSelector.AutoOrderToTileNow)).CancelIfAnyArgNull(); SyncMethod.Register(typeof(CaravanMergeUtility), nameof(CaravanMergeUtility.TryMergeSelectedCaravans)).SetContext(SyncContext.WorldSelected); - SyncMethod.Register(typeof(PawnBanishUtility), nameof(PawnBanishUtility.Banish_NewTemp)).CancelIfAnyArgNull(); + SyncMethod.Register(typeof(PawnBanishUtility), nameof(PawnBanishUtility.Banish), [typeof(Pawn), typeof(PlanetTile), typeof(bool)]).CancelIfAnyArgNull(); SyncMethod.Register(typeof(SettlementUtility), nameof(SettlementUtility.Attack)).CancelIfAnyArgNull(); SyncMethod.Register(typeof(WITab_Caravan_Gear), nameof(WITab_Caravan_Gear.TryEquipDraggedItem)).SetContext(SyncContext.WorldSelected).CancelIfNoSelectedWorldObjects().CancelIfAnyArgNull(); @@ -266,15 +266,17 @@ public static void Init() SyncMethod.Lambda(typeof(CompObelisk), nameof(CompObelisk.CompGetGizmosExtra), 0).SetDebugOnly(); // Trigger interaction effect SyncMethod.Lambda(typeof(Pawn_NeedsTracker), nameof(Pawn_NeedsTracker.GetGizmos), 0).SetDebugOnly(); // +5% mech energy SyncMethod.Lambda(typeof(Pawn_NeedsTracker), nameof(Pawn_NeedsTracker.GetGizmos), 1).SetDebugOnly(); // -5% mech energy - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 4).SetDebugOnly(); // Cause mental break - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 6).SetDebugOnly(); // Make random pawn hungry - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 8).SetDebugOnly(); // Kill random pawn - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 9).SetDebugOnly(); // Kill all non-slave pawns - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 10).SetDebugOnly(); // Harm random pawn - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 11).SetDebugOnly(); // Down random pawn - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 13).SetDebugOnly(); // Plague on random pawn - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 15).SetDebugOnly(); // Teleport to destination - SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 16).SetDebugOnly(); // +20% psyfocus + + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 4).SetDebugOnly(); // End shuttle cooldown SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 4).SetDebugOnly(); // End shuttle cooldown + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 5).SetDebugOnly(); // Cause mental break SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 5).SetDebugOnly(); // Cause mental break + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 7).SetDebugOnly(); // Make random pawn hungry SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 7).SetDebugOnly(); // Make random pawn hungry + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 9).SetDebugOnly(); // Kill random pawn SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 9).SetDebugOnly(); // Kill random pawn + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 10).SetDebugOnly(); // Kill all non-slave pawns SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 10).SetDebugOnly(); // Kill all non-slave pawns + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 11).SetDebugOnly(); // Harm random pawn SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 11).SetDebugOnly(); // Harm random pawn + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 12).SetDebugOnly(); // Down random pawn SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 12).SetDebugOnly(); // Down random pawn + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 14).SetDebugOnly(); // Plague on random pawn SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 14).SetDebugOnly(); // Plague on random pawn + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 16).SetDebugOnly(); // Teleport to destination SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 16).SetDebugOnly(); // Teleport to destination + SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 17).SetDebugOnly(); // +20% psyfocus SyncMethod.Lambda(typeof(Caravan), nameof(Caravan.GetGizmos), 17).SetDebugOnly(); // +20% psyfocus SyncMethod.Register(typeof(Caravan_ForageTracker), nameof(Caravan_ForageTracker.Forage)).SetDebugOnly(); // Dev forage SyncMethod.Lambda(typeof(EnterCooldownComp), nameof(EnterCooldownComp.GetGizmos), 0).SetDebugOnly(); // Set enter cooldown to 1 hour SyncMethod.Lambda(typeof(EnterCooldownComp), nameof(EnterCooldownComp.GetGizmos), 1).SetDebugOnly(); // Reset enter cooldown @@ -326,7 +328,7 @@ public static void Init() SyncMethod.Lambda(typeof(Building_MechCharger), nameof(Building_MechCharger.GetGizmos), 1).SetDebugOnly(); // Waste 25% SyncMethod.Lambda(typeof(Building_MechCharger), nameof(Building_MechCharger.GetGizmos), 2).SetDebugOnly(); // Waste 0% SyncMethod.Register(typeof(Building_MechCharger), nameof(Building_MechCharger.GenerateWastePack)).SetDebugOnly(); // Generate waste, lambdaOrdinal: 3 - SyncMethod.Lambda(typeof(Building_MechCharger), nameof(Building_MechCharger.GetGizmos), 4).SetDebugOnly(); // Charge 100% + SyncMethod.Lambda(typeof(Building_MechCharger), nameof(Building_MechCharger.GetGizmos), 3).SetDebugOnly(); // Charge 100% // Gestator SyncMethod.Lambda(typeof(Building_MechGestator), nameof(Building_MechGestator.GetGizmos), 0).SetDebugOnly(); // Generate 5 waste SyncMethod.Register(typeof(Bill_Mech), nameof(Bill_Mech.ForceCompleteAllCycles)).SetDebugOnly(); // Called from Building_MechGestator.GetGizmos @@ -400,7 +402,7 @@ public static void Init() SyncMethod.Register(typeof(PitBurrow), nameof(PitBurrow.Collapse)).SetDebugOnly(); SyncMethod.Lambda(typeof(PitBurrow), nameof(PitBurrow.GetGizmos), 0).SetDebugOnly(); // Spawn fleshbeast SyncMethod.Register(typeof(PitGate), nameof(PitGate.TryFireIncident)).SetDebugOnly(); // Trigger incident with specific point value/with natural point value - SyncMethod.Lambda(typeof(PitGate), nameof(PitGate.GetGizmos), 4).SetDebugOnly(); // End cooldown + SyncMethod.Lambda(typeof(PitGate), nameof(PitGate.GetGizmos), 3).SetDebugOnly(); // End cooldown SyncMethod.Register(typeof(PitGate), nameof(PitGate.BeginCollapsing)).SetDebugOnly(); // Bioferrite harvester diff --git a/Source/Client/Syncing/SyncUtil.cs b/Source/Client/Syncing/SyncUtil.cs index 1d78f502..0de6aae5 100644 --- a/Source/Client/Syncing/SyncUtil.cs +++ b/Source/Client/Syncing/SyncUtil.cs @@ -1,5 +1,6 @@ using HarmonyLib; using Multiplayer.API; +using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; using RimWorld.Planet; @@ -68,7 +69,7 @@ public static SyncHandler HandleCmd(ByteReader data) if (handler.context.HasFlag(SyncContext.WorldSelected)) { List selected = SyncSerialization.ReadSync>(data); - Find.WorldSelector.selected = selected.Cast().AllNotNull().ToList(); + FieldRefs.worldSelected(Find.WorldSelector) = selected.Cast().AllNotNull().ToList(); } if (handler.context.HasFlag(SyncContext.QueueOrder_Down)) @@ -86,7 +87,7 @@ public static SyncHandler HandleCmd(ByteReader data) MouseCellPatch.result = null; KeyIsDownPatch.shouldQueue = null; Find.Selector.selected = prevSelected; - Find.WorldSelector.selected = prevWorldSelected; + FieldRefs.worldSelected(Find.WorldSelector) = prevWorldSelected; } return handler; diff --git a/Source/Client/UI/IngameUI.cs b/Source/Client/UI/IngameUI.cs index d936c74c..02fc24bc 100644 --- a/Source/Client/UI/IngameUI.cs +++ b/Source/Client/UI/IngameUI.cs @@ -165,7 +165,7 @@ private static void IndicatorInfo(out Color color, out string text, out bool slo color = new Color(0.0f, 0.8f, 0.0f); } - if (!WorldRendererUtility.WorldRenderedNow) + if (!WorldRendererUtility.WorldRendered) text += $"\n\nCurrent map avg TPS: {tps:0.00}"; } diff --git a/Source/Client/UI/LocationPings.cs b/Source/Client/UI/LocationPings.cs index 3a82e36e..a78944e2 100644 --- a/Source/Client/UI/LocationPings.cs +++ b/Source/Client/UI/LocationPings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; @@ -22,7 +22,7 @@ public void UpdatePing() if (pingsEnabled) if (MultiplayerStatic.PingKeyDef.JustPressed || KeyDown(Multiplayer.settings.sendPingButton)) { - if (WorldRendererUtility.WorldRenderedNow) + if (WorldRendererUtility.WorldRendered) PingLocation(-1, GenWorld.MouseTile(), Vector3.zero); else if (Find.CurrentMap != null) PingLocation(Find.CurrentMap.uniqueID, 0, UI.MouseMapPosition()); diff --git a/Source/Client/UI/PlayerCursors.cs b/Source/Client/UI/PlayerCursors.cs index 99b45190..a0f8afce 100644 --- a/Source/Client/UI/PlayerCursors.cs +++ b/Source/Client/UI/PlayerCursors.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Multiplayer.Common; using RimWorld.Planet; @@ -36,7 +36,7 @@ private void SendCursor() var writer = new ByteWriter(); writer.WriteByte(cursorSeq++); - if (Find.CurrentMap != null && !WorldRendererUtility.WorldRenderedNow) + if (Find.CurrentMap != null && !WorldRendererUtility.WorldRendered) { writer.WriteByte((byte)Find.CurrentMap.Index); @@ -67,7 +67,7 @@ private void SendSelected() var writer = new ByteWriter(); int mapId = Find.CurrentMap?.Index ?? -1; - if (WorldRendererUtility.WorldRenderedNow) mapId = -1; + if (WorldRendererUtility.WorldRendered) mapId = -1; bool reset = false; diff --git a/Source/Client/Util/FieldRef.cs b/Source/Client/Util/FieldRef.cs new file mode 100644 index 00000000..ec87d2d1 --- /dev/null +++ b/Source/Client/Util/FieldRef.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using HarmonyLib; +using RimWorld.Planet; + +namespace Multiplayer.Client.Util; + +public static class FieldRefs +{ + public static AccessTools.FieldRef> worldSelected = AccessTools.FieldRefAccess>(nameof(WorldSelector.selected)); +} diff --git a/Source/Client/Util/SimpleProfiler.cs b/Source/Client/Util/SimpleProfiler.cs index b1745d12..20b8c193 100644 --- a/Source/Client/Util/SimpleProfiler.cs +++ b/Source/Client/Util/SimpleProfiler.cs @@ -1,4 +1,4 @@ -using RimWorld.Planet; +using RimWorld.Planet; using System; using System.Collections; using System.Collections.Generic; @@ -158,7 +158,7 @@ obj is Rot4 if (f.Name == "calcGrid" && (f.DeclaringType == typeof(PathFinder) || - f.DeclaringType == typeof(WorldPathFinder) + f.DeclaringType == typeof(WorldPathing) )) continue; builder.Append(' ', depth); @@ -177,7 +177,7 @@ obj is Rot4 f.FieldType == typeof(FogGrid) || f.FieldType == typeof(ListerThings) || f.FieldType == typeof(LinkGrid) || - f.FieldType == typeof(GlowFlooder) || + //f.FieldType == typeof(GlowFlooder) || f.FieldType == typeof(MapCellsInRandomOrder) || f.FieldType == typeof(GlowGrid) || f.FieldType == typeof(DeepResourceGrid) || diff --git a/Source/Server/Server.csproj b/Source/Server/Server.csproj index 6b20c7ac..889a8af5 100644 --- a/Source/Server/Server.csproj +++ b/Source/Server/Server.csproj @@ -16,7 +16,7 @@ - + diff --git a/Source/Tests/Tests.csproj b/Source/Tests/Tests.csproj index 1469f60f..5eb357a6 100644 --- a/Source/Tests/Tests.csproj +++ b/Source/Tests/Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/Source/TestsOnMono/TestsOnMono.csproj b/Source/TestsOnMono/TestsOnMono.csproj index 23a40a92..b3516c41 100644 --- a/Source/TestsOnMono/TestsOnMono.csproj +++ b/Source/TestsOnMono/TestsOnMono.csproj @@ -10,7 +10,7 @@ - + From 000bd7d8ffe69b15d073d8df0a0575c28ebe7d48 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 16:37:15 +1000 Subject: [PATCH 03/38] Fixed world not generating. --- Source/Client/Debug/DebugPatches.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Client/Debug/DebugPatches.cs b/Source/Client/Debug/DebugPatches.cs index 04bcb062..8c06c714 100644 --- a/Source/Client/Debug/DebugPatches.cs +++ b/Source/Client/Debug/DebugPatches.cs @@ -61,8 +61,7 @@ static void Postfix() { if (SetupQuickTestPatch.marker) { - Find.GameInitData.startingTile = 501; - Find.WorldGrid[Find.GameInitData.startingTile].hilliness = Hilliness.SmallHills; + Find.GameInitData.startingTile = 400; } } } From 681851076b4fbee6097dad6cf3fa6fc272de9fb3 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 17:15:22 +1000 Subject: [PATCH 04/38] Init creation --- Source/Common/DeferredStackTracingImpl.cs | 408 ++++++++++++++++------ 1 file changed, 296 insertions(+), 112 deletions(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index 834d5926..c51cd0c0 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using Verse; namespace Multiplayer.Client.Desyncs; @@ -36,113 +37,240 @@ struct AddrInfo public static unsafe 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) + // Critical safety checks - prevent null reference exceptions + if (traceIn == null) { - ret = *(long*)(stck + 8); + // Log error and return safely + if (MpVersion.IsDebug) + Log.Error("DeferredStackTracing: traceIn array is null"); + return 0; + } - int index = (int)(HashAddr((ulong)ret) >> shift); - ref var info = ref hashtable[index]; - int colls = 0; + // Additional safety check for Native.LmfPtr + if (Native.LmfPtr == 0) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: Native.LmfPtr is not initialized"); + return 0; + } - // Open addressing - while (info.addr != 0 && info.addr != ret) + try + { + long[] trace = traceIn; + long rbp = GetRbp(); + long stck = rbp; + + // Safety check for initial RBP + if (rbp == 0) { - index = (index + 1) & indexmask; - info = ref hashtable[index]; - colls++; + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: Initial RBP is null"); + return 0; } - if (colls > collisions) - collisions = colls; - - long stackUsage = 0; + rbp = *(long*)rbp; - if (info.addr != 0) - stackUsage = info.stackUsage; - else - stackUsage = UpdateNewElement(ref info, ret); + int indexmask = hashtableSize - 1; + int shift = hashtableShift; - if (stackUsage == NotJit) - { - // LMF (Last Managed Frame) layout on x64: - // previous - // rbp - // rsp + long ret; + long lmfPtr = *(long*)Native.LmfPtr; - lmfPtr = *(long*)lmfPtr; - var lmfRbp = *(long*)(lmfPtr + 8); + int depth = 0; - if (lmfPtr == 0 || lmfRbp == 0) + while (true) + { + // Safety check for stack pointer + if (stck == 0) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: Stack pointer became null"); break; + } - rbp = lmfRbp; - stck = *(long*)(lmfPtr + 16) - 16; + ret = *(long*)(stck + 8); - continue; - } + 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++; + } - trace[depth] = ret; + if (colls > collisions) + collisions = colls; - // info.nameHash == 0 marks methods to skip - if (depth < HashInfluence && info.nameHash != 0) - hash = HashCombineInt(hash, (int)info.nameHash); + long stackUsage = 0; - if (info.nameHash != 0 && ++depth == MaxDepth) - break; + if (info.addr != 0) + stackUsage = info.stackUsage; + else + { + try + { + stackUsage = UpdateNewElement(ref info, ret); + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: Failed to update new element for addr {ret:X}: {ex.Message}"); + stackUsage = NotJit; // Treat as non-JIT to continue + } + } + + if (stackUsage == NotJit) + { + // LMF (Last Managed Frame) layout on x64: + // previous + // rbp + // rsp + + // Safety check for LMF operations + if (lmfPtr == 0) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: LMF pointer is null, stopping trace"); + break; + } + + try + { + lmfPtr = *(long*)lmfPtr; + if (lmfPtr == 0) + break; + + var lmfRbp = *(long*)(lmfPtr + 8); + if (lmfRbp == 0) + break; + + rbp = lmfRbp; + stck = *(long*)(lmfPtr + 16) - 16; + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: LMF access failed: {ex.Message}"); + break; + } + + continue; + } + + // Safety check for trace array bounds + if (depth >= trace.Length) + { + if (MpVersion.IsDebug) + Log.Warning($"DeferredStackTracing: Trace depth {depth} exceeds array length {trace.Length}"); + break; + } - if (stackUsage == RbpBased) - { - stck = rbp; - rbp = *(long*)rbp; - continue; - } + trace[depth] = ret; - stck += 8; + // info.nameHash == 0 marks methods to skip + if (depth < HashInfluence && info.nameHash != 0) + hash = HashCombineInt(hash, (int)info.nameHash); - if ((stackUsage & UsesRbpAsGpr) != 0) - { - if ((stackUsage & UsesRbx) != 0) - rbp = *(long*)(stck + 16); - else - rbp = *(long*)(stck + 8); + if (info.nameHash != 0 && ++depth == MaxDepth) + break; - stackUsage &= RbpInfoClearMask; + if (stackUsage == RbpBased) + { + // Safety check for RBP operations + if (rbp == 0) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: RBP became null during RBP-based tracing"); + break; + } + + try + { + stck = rbp; + rbp = *(long*)rbp; + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: RBP access failed: {ex.Message}"); + break; + } + continue; + } + + stck += 8; + + if ((stackUsage & UsesRbpAsGpr) != 0) + { + try + { + if ((stackUsage & UsesRbx) != 0) + rbp = *(long*)(stck + 16); + else + rbp = *(long*)(stck + 8); + + stackUsage &= RbpInfoClearMask; + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: GPR RBP access failed: {ex.Message}"); + break; + } + } + + stck += stackUsage; } - stck += stackUsage; + return depth; + } + catch (Exception ex) + { + // Catch-all for any remaining exceptions + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: Unexpected error in TraceImpl: {ex}"); + return 0; } - - return depth; } static long UpdateNewElement(ref AddrInfo info, long ret) { - long stackUsage = GetStackUsage(ret); + try + { + long stackUsage = GetStackUsage(ret); - info.addr = ret; - info.stackUsage = stackUsage; + info.addr = ret; + info.stackUsage = stackUsage; - var rawName = Native.MethodNameFromAddr(ret, true); // Use the original instead of replacement for hashing - info.nameHash = rawName != null ? Native.GetMethodAggressiveInlining(ret) ? 0 : StableStringHash(rawName) : 1; + // Safety check for Native method calls + try + { + var rawName = Native.MethodNameFromAddr(ret, true); // Use the original instead of replacement for hashing + info.nameHash = rawName != null ? Native.GetMethodAggressiveInlining(ret) ? 0 : StableStringHash(rawName) : 1; + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Warning($"DeferredStackTracing: Failed to get method name for addr {ret:X}: {ex.Message}"); + info.nameHash = 1; // Default hash value + } - hashtableEntries++; - if (hashtableEntries > hashtableSize * LoadFactor) - ResizeHashtable(); + hashtableEntries++; + if (hashtableEntries > hashtableSize * LoadFactor) + ResizeHashtable(); - return stackUsage; + return stackUsage; + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: Failed to update new element: {ex}"); + return NotJit; // Return safe value to continue + } } static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485; @@ -182,55 +310,111 @@ static int ResizeHashtable() static unsafe long GetStackUsage(long addr) { - var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); + try + { + // Safety check for Native.DomainPtr + if (Native.DomainPtr == IntPtr.Zero) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: Native.DomainPtr is null"); + return NotJit; + } - if (ji == IntPtr.Zero) - return NotJit; + var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); - var start = (uint*)Native.mono_jit_info_get_code_start(ji); - long usage = 0; + if (ji == IntPtr.Zero) + return NotJit; - 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); - } + var start = (uint*)Native.mono_jit_info_get_code_start(ji); + + // Safety check for code start pointer + if (start == null) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: Code start pointer is null"); + return NotJit; + } - if (usage != 0) - { - CheckRbpUsage(start, ref usage); - return usage; - } + long usage = 0; - // push rbp (55) - if (*(byte*)start == 0x55) - return RbpBased; + 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); + } - throw new Exception($"Deferred stack tracing: Unknown function header {*start} {Native.MethodNameFromAddr(addr, false)}"); + if (usage != 0) + { + CheckRbpUsage(start, ref usage); + return usage; + } + + // push rbp (55) + if (*(byte*)start == 0x55) + return RbpBased; + + // Instead of throwing exception, log warning and return safe value + if (MpVersion.IsDebug) + { + try + { + var methodName = Native.MethodNameFromAddr(addr, false); + Log.Warning($"DeferredStackTracing: Unknown function header {*start:X} for method {methodName ?? "unknown"}"); + } + catch + { + Log.Warning($"DeferredStackTracing: Unknown function header {*start:X} at addr {addr:X}"); + } + } + return NotJit; // Return safe value instead of throwing + } + catch (Exception ex) + { + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: Exception in GetStackUsage for addr {addr:X}: {ex}"); + return NotJit; // Return safe value on any exception + } } 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) + try { - stackUsage |= UsesRbpAsGpr; + // Safety check for null pointer + if (at == null) + { + if (MpVersion.IsDebug) + Log.Warning("DeferredStackTracing: CheckRbpUsage called with null pointer"); + return; + } + + // 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; + } } - else if (*at == 0x241C8948 && *(at + 1) == 0x246C8948) + catch (Exception ex) { - stackUsage |= UsesRbpAsGpr; - stackUsage |= UsesRbx; + if (MpVersion.IsDebug) + Log.Error($"DeferredStackTracing: Exception in CheckRbpUsage: {ex.Message}"); + // Don't modify stackUsage on error } } From f3f1033635db4f15a19de5bb61c157ab7ece47b1 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 17:43:50 +1000 Subject: [PATCH 05/38] Revert "Init creation" This reverts commit 681851076b4fbee6097dad6cf3fa6fc272de9fb3. --- Source/Common/DeferredStackTracingImpl.cs | 408 ++++++---------------- 1 file changed, 112 insertions(+), 296 deletions(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index c51cd0c0..834d5926 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.CompilerServices; -using Verse; namespace Multiplayer.Client.Desyncs; @@ -37,240 +36,113 @@ struct AddrInfo public static unsafe int TraceImpl(long[] traceIn, ref int hash) { - // Critical safety checks - prevent null reference exceptions - if (traceIn == null) - { - // Log error and return safely - if (MpVersion.IsDebug) - Log.Error("DeferredStackTracing: traceIn array is null"); - return 0; - } + long[] trace = traceIn; + long rbp = GetRbp(); + long stck = rbp; + rbp = *(long*)rbp; - // Additional safety check for Native.LmfPtr - if (Native.LmfPtr == 0) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: Native.LmfPtr is not initialized"); - return 0; - } + int indexmask = hashtableSize - 1; + int shift = hashtableShift; + + long ret; + long lmfPtr = *(long*)Native.LmfPtr; - try + int depth = 0; + + while (true) { - long[] trace = traceIn; - long rbp = GetRbp(); - long stck = rbp; - - // Safety check for initial RBP - if (rbp == 0) + 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) { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: Initial RBP is null"); - return 0; + index = (index + 1) & indexmask; + info = ref hashtable[index]; + colls++; } - rbp = *(long*)rbp; - - int indexmask = hashtableSize - 1; - int shift = hashtableShift; + if (colls > collisions) + collisions = colls; - long ret; - long lmfPtr = *(long*)Native.LmfPtr; + long stackUsage = 0; - int depth = 0; + if (info.addr != 0) + stackUsage = info.stackUsage; + else + stackUsage = UpdateNewElement(ref info, ret); - while (true) + if (stackUsage == NotJit) { - // Safety check for stack pointer - if (stck == 0) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: Stack pointer became null"); - break; - } + // LMF (Last Managed Frame) layout on x64: + // previous + // rbp + // rsp + + lmfPtr = *(long*)lmfPtr; + var lmfRbp = *(long*)(lmfPtr + 8); - ret = *(long*)(stck + 8); + if (lmfPtr == 0 || lmfRbp == 0) + break; - int index = (int)(HashAddr((ulong)ret) >> shift); - ref var info = ref hashtable[index]; - int colls = 0; + rbp = lmfRbp; + stck = *(long*)(lmfPtr + 16) - 16; - // Open addressing - while (info.addr != 0 && info.addr != ret) - { - index = (index + 1) & indexmask; - info = ref hashtable[index]; - colls++; - } + continue; + } - if (colls > collisions) - collisions = colls; + trace[depth] = ret; - long stackUsage = 0; + // info.nameHash == 0 marks methods to skip + if (depth < HashInfluence && info.nameHash != 0) + hash = HashCombineInt(hash, (int)info.nameHash); - if (info.addr != 0) - stackUsage = info.stackUsage; - else - { - try - { - stackUsage = UpdateNewElement(ref info, ret); - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: Failed to update new element for addr {ret:X}: {ex.Message}"); - stackUsage = NotJit; // Treat as non-JIT to continue - } - } - - if (stackUsage == NotJit) - { - // LMF (Last Managed Frame) layout on x64: - // previous - // rbp - // rsp - - // Safety check for LMF operations - if (lmfPtr == 0) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: LMF pointer is null, stopping trace"); - break; - } - - try - { - lmfPtr = *(long*)lmfPtr; - if (lmfPtr == 0) - break; - - var lmfRbp = *(long*)(lmfPtr + 8); - if (lmfRbp == 0) - break; - - rbp = lmfRbp; - stck = *(long*)(lmfPtr + 16) - 16; - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: LMF access failed: {ex.Message}"); - break; - } - - continue; - } - - // Safety check for trace array bounds - if (depth >= trace.Length) - { - if (MpVersion.IsDebug) - Log.Warning($"DeferredStackTracing: Trace depth {depth} exceeds array length {trace.Length}"); - break; - } + if (info.nameHash != 0 && ++depth == MaxDepth) + break; - trace[depth] = ret; + if (stackUsage == RbpBased) + { + stck = rbp; + rbp = *(long*)rbp; + continue; + } - // info.nameHash == 0 marks methods to skip - if (depth < HashInfluence && info.nameHash != 0) - hash = HashCombineInt(hash, (int)info.nameHash); + stck += 8; - if (info.nameHash != 0 && ++depth == MaxDepth) - break; + if ((stackUsage & UsesRbpAsGpr) != 0) + { + if ((stackUsage & UsesRbx) != 0) + rbp = *(long*)(stck + 16); + else + rbp = *(long*)(stck + 8); - if (stackUsage == RbpBased) - { - // Safety check for RBP operations - if (rbp == 0) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: RBP became null during RBP-based tracing"); - break; - } - - try - { - stck = rbp; - rbp = *(long*)rbp; - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: RBP access failed: {ex.Message}"); - break; - } - continue; - } - - stck += 8; - - if ((stackUsage & UsesRbpAsGpr) != 0) - { - try - { - if ((stackUsage & UsesRbx) != 0) - rbp = *(long*)(stck + 16); - else - rbp = *(long*)(stck + 8); - - stackUsage &= RbpInfoClearMask; - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: GPR RBP access failed: {ex.Message}"); - break; - } - } - - stck += stackUsage; + stackUsage &= RbpInfoClearMask; } - return depth; - } - catch (Exception ex) - { - // Catch-all for any remaining exceptions - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: Unexpected error in TraceImpl: {ex}"); - return 0; + stck += stackUsage; } + + return depth; } static long UpdateNewElement(ref AddrInfo info, long ret) { - try - { - long stackUsage = GetStackUsage(ret); + long stackUsage = GetStackUsage(ret); - info.addr = ret; - info.stackUsage = stackUsage; + info.addr = ret; + info.stackUsage = stackUsage; - // Safety check for Native method calls - try - { - var rawName = Native.MethodNameFromAddr(ret, true); // Use the original instead of replacement for hashing - info.nameHash = rawName != null ? Native.GetMethodAggressiveInlining(ret) ? 0 : StableStringHash(rawName) : 1; - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Warning($"DeferredStackTracing: Failed to get method name for addr {ret:X}: {ex.Message}"); - info.nameHash = 1; // Default hash value - } + var rawName = Native.MethodNameFromAddr(ret, true); // Use the original instead of replacement for hashing + info.nameHash = rawName != null ? Native.GetMethodAggressiveInlining(ret) ? 0 : StableStringHash(rawName) : 1; - hashtableEntries++; - if (hashtableEntries > hashtableSize * LoadFactor) - ResizeHashtable(); + hashtableEntries++; + if (hashtableEntries > hashtableSize * LoadFactor) + ResizeHashtable(); - return stackUsage; - } - catch (Exception ex) - { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: Failed to update new element: {ex}"); - return NotJit; // Return safe value to continue - } + return stackUsage; } static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485; @@ -310,111 +182,55 @@ static int ResizeHashtable() static unsafe long GetStackUsage(long addr) { - try - { - // Safety check for Native.DomainPtr - if (Native.DomainPtr == IntPtr.Zero) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: Native.DomainPtr is null"); - return NotJit; - } - - var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); + var ji = Native.mono_jit_info_table_find(Native.DomainPtr, (IntPtr)addr); - if (ji == IntPtr.Zero) - return NotJit; + if (ji == IntPtr.Zero) + return NotJit; - var start = (uint*)Native.mono_jit_info_get_code_start(ji); - - // Safety check for code start pointer - if (start == null) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: Code start pointer is null"); - return NotJit; - } - - 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; - } + var start = (uint*)Native.mono_jit_info_get_code_start(ji); + long usage = 0; - // push rbp (55) - if (*(byte*)start == 0x55) - return RbpBased; - - // Instead of throwing exception, log warning and return safe value - if (MpVersion.IsDebug) - { - try - { - var methodName = Native.MethodNameFromAddr(addr, false); - Log.Warning($"DeferredStackTracing: Unknown function header {*start:X} for method {methodName ?? "unknown"}"); - } - catch - { - Log.Warning($"DeferredStackTracing: Unknown function header {*start:X} at addr {addr:X}"); - } - } - return NotJit; // Return safe value instead of throwing + 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); } - catch (Exception ex) + + if (usage != 0) { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: Exception in GetStackUsage for addr {addr:X}: {ex}"); - return NotJit; // Return safe value on any exception + 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) { - try + // 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) { - // Safety check for null pointer - if (at == null) - { - if (MpVersion.IsDebug) - Log.Warning("DeferredStackTracing: CheckRbpUsage called with null pointer"); - return; - } - - // 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; - } + stackUsage |= UsesRbpAsGpr; } - catch (Exception ex) + else if (*at == 0x241C8948 && *(at + 1) == 0x246C8948) { - if (MpVersion.IsDebug) - Log.Error($"DeferredStackTracing: Exception in CheckRbpUsage: {ex.Message}"); - // Don't modify stackUsage on error + stackUsage |= UsesRbpAsGpr; + stackUsage |= UsesRbx; } } From 3a5ed0c38733fb0b7481395ac866ad735525f6f3 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 21:32:51 +1000 Subject: [PATCH 06/38] Fixed LmfPtr offset values - maybe Need to test the linux and osx versions. --- Source/Common/Native.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Common/Native.cs b/Source/Common/Native.cs index 915e232a..f578e3d6 100644 --- a/Source/Common/Native.cs +++ b/Source/Common/Native.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Threading; using HarmonyLib; +using Verse; namespace Multiplayer.Client { @@ -59,12 +60,13 @@ public static unsafe void InitLmfPtr(NativeOS os) // Struct offset found manually // Navigate by string: "Handle Stack" + // Updated for RimWorld 1.6 (Unity 2022.3.35f1, Mono 6.13.0) if (os == NativeOS.Linux) - LmfPtr = threadInfoPtr + 0x480 - 8 * 4; + LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) else if (os == NativeOS.Windows) - LmfPtr = threadInfoPtr + 0x448 - 8 * 4; + LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. else if (os == NativeOS.OSX) - LmfPtr = threadInfoPtr + 0x418 - 8 * 4; + LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) else if (os == NativeOS.Dummy) { LmfPtr = (long)Marshal.AllocHGlobal(3 * 8); From c3a282aac0dff329701bec24394db0897b78fbf0 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 29 Jun 2025 23:18:15 +1000 Subject: [PATCH 07/38] Fixed faction repeater --- Source/Client/Factions/FactionRepeater.cs | 43 +++++++++++++++++++++++ Source/Common/Native.cs | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Source/Client/Factions/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs index 907d5370..fa1bd8d2 100644 --- a/Source/Client/Factions/FactionRepeater.cs +++ b/Source/Client/Factions/FactionRepeater.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; using HarmonyLib; using Multiplayer.Client.Factions; using RimWorld; @@ -175,4 +178,44 @@ ref ignore ); } + // Fix for RimWorld 1.6 bug where HistoryAutoRecorder.Tick() calls .Last() on empty collection + [HarmonyPatch(typeof(HistoryAutoRecorder), nameof(HistoryAutoRecorder.Tick))] + static class HistoryAutoRecorderTickPatch + { + // Transpiler to fix the .Last() call on potentially empty collection + static IEnumerable Transpiler(IEnumerable instructions, ILGenerator il) + { + var safeLastMethod = AccessTools.Method(typeof(HistoryAutoRecorderTickPatch), nameof(SafeLast)); + + var codes = new List(instructions); + + + for (int i = 0; i < codes.Count; i++) + { + var instruction = codes[i]; + + if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo methodInfo) + { + // Check if this is the generic Enumerable.Last(...) method. + if (methodInfo.Name == nameof(Enumerable.Last) && + methodInfo.DeclaringType == typeof(Enumerable) && + methodInfo.IsGenericMethod) + { + // Found it, replace the operand with our safe method. + instruction.operand = safeLastMethod; + break; + } + } + } + + return codes; + } + + // Safe version of .Last() that returns 0f for empty collections + static float SafeLast(IEnumerable source) + { + return source.Any() ? source.Last() : 0f; + } + } + } diff --git a/Source/Common/Native.cs b/Source/Common/Native.cs index f578e3d6..0f508564 100644 --- a/Source/Common/Native.cs +++ b/Source/Common/Native.cs @@ -64,7 +64,7 @@ public static unsafe void InitLmfPtr(NativeOS os) if (os == NativeOS.Linux) LmfPtr = threadInfoPtr + 0x450; // Updated: 1.5 was 0x460, -16 bytes = 0x450 (following Windows pattern) else if (os == NativeOS.Windows) - LmfPtr = threadInfoPtr + 0x418; // Updated for 1.6. Seems to work so far. + LmfPtr = threadInfoPtr + 0x418; // Updated: 1.5 was 0x418 else if (os == NativeOS.OSX) LmfPtr = threadInfoPtr + 0x3E8; // Updated: 1.5 was 0x3F8, -16 bytes = 0x3E8 (following Windows pattern) else if (os == NativeOS.Dummy) From 966757342ef07783b1e326797140d908adb4ee91 Mon Sep 17 00:00:00 2001 From: Reznal Date: Mon, 30 Jun 2025 10:44:31 +1000 Subject: [PATCH 08/38] Fixed stack tracing --- Source/Common/DeferredStackTracingImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index 834d5926..56fe3dc2 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -238,7 +238,7 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage) static unsafe long GetRbp() { long rbp = 0; - return *(&rbp + 1); + return *(&rbp + 4); // Use offset 4 which works consistently in RimWorld 1.6 } public static int HashCombineInt(int seed, int value) From 588043f2bf536bfe32854733d8d47390333c2a16 Mon Sep 17 00:00:00 2001 From: Reznal Date: Mon, 30 Jun 2025 17:00:35 +1000 Subject: [PATCH 09/38] Patch to fix dev palette being open on loading multiplayer world. --- Source/Client/Patches/Patches.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 7cfe8de8..48cde0ff 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -591,4 +591,21 @@ static class MoteAttachLinkUsesTruePosition static void Finalizer() => DrawPosPatch.returnTruePosition = false; } + + [HarmonyPatch(typeof(DebugWindowsOpener), nameof(DebugWindowsOpener.TryOpenOrClosePalette))] + static class DebugWindowsOpenerReloadingPatch + { + static bool Prefix() + { + // During multiplayer reload, Find.World can be null when UIRoot_Play.Init() calls TryOpenOrClosePalette() + // This prevents the Dialog_DevPalette from being opened, which would cause a null reference exception + // in Window.PreOpen() when it tries to access Find.WorldSelector + if (Multiplayer.reloading) + { + return false; // Skip opening/closing the dev palette during multiplayer reload + } + + return true; // Execute normally + } + } } From f7d57ba18c020c225b541887a5f420ea545b86cf Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 2 Jul 2025 08:59:48 +1000 Subject: [PATCH 10/38] Updated debug window to be more readable and user friendly --- Source/Client/UI/IngameDebug.cs | 31 +- Source/Client/UI/IngameUI.cs | 10 +- Source/Client/UI/SyncDebugPanel.cs | 666 +++++++++++++++++++++++++++++ 3 files changed, 679 insertions(+), 28 deletions(-) create mode 100644 Source/Client/UI/SyncDebugPanel.cs diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index c2f00fd2..7e498230 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Text; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; @@ -102,30 +103,12 @@ internal static void DoDebugPrintout() // RandGetValuePatch.tracesThistick = 0; } - internal static float DoDevInfo(float y) + // Original comprehensive debug information method + // Now replaced by SyncDebugPanel.DoSyncDebugPanel for enhanced UI/UX + // DoDebugPrintout() disabled to prevent overlap with new enhanced panel + public static float DoDevInfo(float y) { - float x = UI.screenWidth - BtnWidth - BtnMargin; - - if (Multiplayer.ShowDevInfo && Multiplayer.WriterLog != null) - { - if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Write ({Multiplayer.WriterLog.NodeCount})")) - Find.WindowStack.Add(Multiplayer.WriterLog); - - y += BtnHeight; - if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Read ({Multiplayer.ReaderLog.NodeCount})")) - Find.WindowStack.Add(Multiplayer.ReaderLog); - - y += BtnHeight; - var oldGhostMode = Multiplayer.session.ghostModeCheckbox; - Widgets.CheckboxLabeled(new Rect(x, y, BtnWidth, 30f), "Ghost", ref Multiplayer.session.ghostModeCheckbox); - if (oldGhostMode != Multiplayer.session.ghostModeCheckbox) - { - SyncFieldUtil.ClearAllBufferedChanges(); - } - - return BtnHeight * 3; - } - + // Legacy debug display disabled - now handled by SyncDebugPanel return 0; } diff --git a/Source/Client/UI/IngameUI.cs b/Source/Client/UI/IngameUI.cs index 02fc24bc..bf783dbb 100644 --- a/Source/Client/UI/IngameUI.cs +++ b/Source/Client/UI/IngameUI.cs @@ -6,6 +6,7 @@ using UnityEngine; using Verse; using RimWorld.Planet; +using Multiplayer.Client.DebugUi; namespace Multiplayer.Client { @@ -15,7 +16,7 @@ public static class IngameUIPatch public static List> upperLeftDrawers = new() { DoChatAndTicksBehind, - IngameDebug.DoDevInfo, + SyncDebugPanel.DoSyncDebugPanel, // Enhanced expandable debug panel IngameDebug.DoDebugModeLabel, IngameDebug.DoTimeDiffLabel }; @@ -33,9 +34,10 @@ static bool Prefix() { Text.Font = GameFont.Small; - if (MpVersion.IsDebug) { - IngameDebug.DoDebugPrintout(); - } + // Legacy debug printout disabled - now handled by SyncDebugPanel + // if (MpVersion.IsDebug) { + // IngameDebug.DoDebugPrintout(); + // } if (Multiplayer.Client != null && Find.CurrentMap != null && Time.time - lastTicksAt > 0.5f) { diff --git a/Source/Client/UI/SyncDebugPanel.cs b/Source/Client/UI/SyncDebugPanel.cs new file mode 100644 index 00000000..eb93edfe --- /dev/null +++ b/Source/Client/UI/SyncDebugPanel.cs @@ -0,0 +1,666 @@ +using System; +using System.Linq; +using System.Text; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.DebugUi +{ + /// + /// Enhanced expandable debug panel that organizes existing comprehensive debug information + /// with modern UI/UX while preserving all developer functionality + /// + public static class SyncDebugPanel + { + // Panel state + private static bool isExpanded = false; + private static Vector2 scrollPosition = Vector2.zero; + + // Panel dimensions + private const float CompactHeight = 40f; + private const float ExpandedMaxHeight = 400f; + private const float PanelWidth = 350f; + private const float Margin = 8f; + + // Visual constants + private const float SectionSpacing = 15f; + private const float LineHeight = 18f; + private const float LabelColumnWidth = 0.45f; // 45% for labels, 55% for values + + /// + /// Main entry point for the enhanced debug panel + /// + public static float DoSyncDebugPanel(float y) + { + // Safety checks + if (Multiplayer.session == null) return 0; + + try + { + float x = Margin; + float panelHeight = isExpanded ? CalculateExpandedHeight() : CompactHeight; + + Rect panelRect = new Rect(x, y, PanelWidth, panelHeight); + + // Draw panel background + Widgets.DrawBoxSolid(panelRect, new Color(0f, 0f, 0f, 0.7f)); + Widgets.DrawBox(panelRect); + + if (isExpanded) + { + DrawExpandedPanel(panelRect); + } + else + { + DrawCompactPanel(panelRect); + } + + return panelHeight + Margin; + } + catch (Exception ex) + { + // Fallback in case of any errors - don't crash the game + Log.Error($"SyncDebugPanel error: {ex.Message}"); + return 0; + } + } + + /// + /// Draw the compact status summary view + /// + private static void DrawCompactPanel(Rect rect) + { + Rect contentRect = rect.ContractedBy(Margin); + + try + { + // Get status information + var syncStatus = GetSyncStatus(); + var performanceStatus = GetPerformanceStatus(); + var errorStatus = GetErrorStatus(); + var tickStatus = GetTickStatus(); + + // Draw compact status indicators with text values + float currentX = contentRect.x + 2f; + float centerY = contentRect.y + (contentRect.height / 2f); + + // Sync status [🟢 SYNC] + currentX = DrawCompactStatusBadge(currentX, centerY, syncStatus.icon, syncStatus.text, syncStatus.color, syncStatus.tooltip); + currentX += 4f; // spacing between badges + + // Performance status [⚡ 45.2] + currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); + currentX += 4f; + + // Error status [📊 0] + currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); + currentX += 4f; + + // Tick status [🎯 3] + currentX = DrawCompactStatusBadge(currentX, centerY, tickStatus.icon, tickStatus.text, tickStatus.color, tickStatus.tooltip); + + // Expand button [v] + float buttonWidth = 20f; + Rect expandRect = new Rect(contentRect.xMax - buttonWidth - 2f, + contentRect.y + (contentRect.height - 16f) / 2f, + buttonWidth, 16f); + + // Draw expand button as a badge (no border for consistency) + Widgets.DrawBoxSolid(expandRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleCenter)) + { + if (Widgets.ButtonText(expandRect, "v")) + { + isExpanded = true; + } + } + + // Click anywhere else to expand + if (Event.current.type == EventType.MouseDown && + Event.current.button == 0 && + contentRect.Contains(Event.current.mousePosition) && + !expandRect.Contains(Event.current.mousePosition)) + { + isExpanded = true; + Event.current.Use(); + } + } + catch (Exception ex) + { + // Fallback display for compact mode + using (MpStyle.Set(GameFont.Tiny).Set(Color.red).Set(TextAnchor.MiddleLeft)) + { + Widgets.Label(contentRect, $"Debug Panel Error: {ex.Message}"); + } + } + } + + /// + /// Draw a compact status badge with icon and text [🟢 SYNC] + /// + private static float DrawCompactStatusBadge(float x, float centerY, string icon, string text, Color color, string tooltip) + { + // Calculate badge dimensions + string badgeText = $"{icon} {text}"; + float textWidth = Text.CalcSize(badgeText).x + 8f; // padding + float badgeHeight = 16f; + + Rect badgeRect = new Rect(x, centerY - badgeHeight / 2f, textWidth, badgeHeight); + + // Draw badge background (no border for cleaner look) + Widgets.DrawBoxSolid(badgeRect, new Color(0.1f, 0.1f, 0.1f, 0.7f)); + + // Draw badge text + using (MpStyle.Set(GameFont.Tiny).Set(color).Set(TextAnchor.MiddleCenter)) + { + Widgets.Label(badgeRect, badgeText); + } + + // Add tooltip + if (!string.IsNullOrEmpty(tooltip)) + { + TooltipHandler.TipRegion(badgeRect, tooltip); + } + + return x + textWidth; + } + + /// + /// Draw the expanded comprehensive debug view + /// + private static void DrawExpandedPanel(Rect rect) + { + Rect headerRect = new Rect(rect.x, rect.y, rect.width, 30f); + Rect contentRect = new Rect(rect.x, rect.y + 30f, rect.width, rect.height - 30f); + + // Draw header + DrawPanelHeader(headerRect); + + // Draw scrollable content + Rect viewRect = new Rect(0f, 0f, contentRect.width - 16f, GetContentHeight()); + Widgets.BeginScrollView(contentRect, ref scrollPosition, viewRect); + + float currentY = 0f; + + // Status Summary Section + currentY += DrawStatusSummarySection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); + currentY += SectionSpacing; + + // RNG States Section + currentY += DrawRNGStatesSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); + currentY += SectionSpacing; + + // Performance Section + currentY += DrawPerformanceSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); + currentY += SectionSpacing; + + // Network & Sync Section + currentY += DrawNetworkSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); + currentY += SectionSpacing; + + // Detailed Debug Section (existing comprehensive information) + currentY += DrawDetailedDebugSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); + + Widgets.EndScrollView(); + } + + /// + /// Draw the panel header with title and controls + /// + private static void DrawPanelHeader(Rect rect) + { + // Draw border around entire header for expanded mode + Widgets.DrawBox(rect); + + try + { + // Get status information - same as compact mode + var syncStatus = GetSyncStatus(); + var performanceStatus = GetPerformanceStatus(); + var errorStatus = GetErrorStatus(); + var tickStatus = GetTickStatus(); + + // Use SAME content area as compact mode (contract by margin) + Rect contentRect = rect.ContractedBy(Margin); + + // Draw status badges - EXACTLY same layout as compact mode + float currentX = contentRect.x + 2f; // Same margin as compact + float centerY = contentRect.y + (contentRect.height / 2f); + + // Sync status [● SYNC] + currentX = DrawCompactStatusBadge(currentX, centerY, syncStatus.icon, syncStatus.text, syncStatus.color, syncStatus.tooltip); + currentX += 4f; // Same spacing as compact + + // Performance status [▲ 45.2] + currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); + currentX += 4f; + + // Error status [■ 0] + currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); + currentX += 4f; + + // Tick status [♦ 3] + currentX = DrawCompactStatusBadge(currentX, centerY, tickStatus.icon, tickStatus.text, tickStatus.color, tickStatus.tooltip); + + // Collapse button [^] - same style as expand button but no extra border + float buttonWidth = 20f; + Rect collapseRect = new Rect(contentRect.xMax - buttonWidth - 2f, // Same positioning as compact + contentRect.y + (contentRect.height - 16f) / 2f, + buttonWidth, 16f); + + // Draw collapse button with same styling as compact expand button (no border) + Widgets.DrawBoxSolid(collapseRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleCenter)) + { + if (Widgets.ButtonText(collapseRect, "^")) + { + isExpanded = false; + } + } + } + catch (Exception ex) + { + // Fallback to simple header + using (MpStyle.Set(GameFont.Small).Set(Color.white).Set(TextAnchor.MiddleLeft)) + { + Rect titleRect = new Rect(rect.x + Margin, rect.y, rect.width - 100f, rect.height); + Widgets.Label(titleRect, "MULTIPLAYER DEBUG"); + } + + Rect collapseRect = new Rect(rect.xMax - 80f, rect.y + 5f, 70f, 20f); + if (Widgets.ButtonText(collapseRect, "Collapse")) + { + isExpanded = false; + } + } + } + + /// + /// Draw status summary section with key indicators + /// + private static float DrawStatusSummarySection(float x, float y, float width) + { + float startY = y; + + // Section header + y = DrawSectionHeader(x, y, width, "STATUS SUMMARY"); + + var syncStatus = GetSyncStatus(); + var performanceStatus = GetPerformanceStatus(); + var errorStatus = GetErrorStatus(); + var tickStatus = GetTickStatus(); + + // Status lines + y = DrawStatusLine(x, y, width, "Sync Status:", syncStatus.text, syncStatus.color); + y = DrawStatusLine(x, y, width, "Performance:", performanceStatus.text, performanceStatus.color); + y = DrawStatusLine(x, y, width, "Error Rate:", errorStatus.text, errorStatus.color); + y = DrawStatusLine(x, y, width, "Tick Status:", tickStatus.text, tickStatus.color); + + return y - startY; + } + + /// + /// Draw RNG states comparison section + /// + private static float DrawRNGStatesSection(float x, float y, float width) + { + float startY = y; + + y = DrawSectionHeader(x, y, width, "RNG STATES"); + + if (Find.CurrentMap?.AsyncTime() != null) + { + var async = Find.CurrentMap.AsyncTime(); + var worldAsync = Multiplayer.AsyncWorldTime; + + // Map RNG state + string mapRngLow = $"{(uint)async.randState:X8}"; + string mapRngHigh = $"{(uint)(async.randState >> 32):X8}"; + y = DrawStatusLine(x, y, width, "Map RNG:", $"{mapRngHigh} | {mapRngLow}", Color.white); + + // World RNG state + string worldRngLow = $"{(uint)worldAsync.randState:X8}"; + string worldRngHigh = $"{(uint)(worldAsync.randState >> 32):X8}"; + y = DrawStatusLine(x, y, width, "World RNG:", $"{worldRngHigh} | {worldRngLow}", Color.white); + + // Sync indicator + bool rngInSync = async.randState == worldAsync.randState; + Color syncColor = rngInSync ? Color.green : Color.red; + string syncText = rngInSync ? "✓ In Sync" : "✗ Desync Detected"; + y = DrawStatusLine(x, y, width, "RNG Sync:", syncText, syncColor); + } + else + { + y = DrawStatusLine(x, y, width, "RNG States:", "No current map", Color.gray); + } + + return y - startY; + } + + /// + /// Draw performance metrics section + /// + private static float DrawPerformanceSection(float x, float y, float width) + { + float startY = y; + + y = DrawSectionHeader(x, y, width, "PERFORMANCE"); + + // TPS + float tps = IngameUIPatch.tps; + Color tpsColor = tps > 40f ? Color.green : tps > 20f ? Color.yellow : Color.red; + y = DrawStatusLine(x, y, width, "Map TPS:", $"{tps:F1}", tpsColor); + + // Frame time + float frameTime = Time.deltaTime * 1000f; + Color frameColor = frameTime < 20f ? Color.green : frameTime < 35f ? Color.yellow : Color.red; + y = DrawStatusLine(x, y, width, "Frame Time:", $"{frameTime:F1}ms", frameColor); + + // Server time per tick + float serverTpt = TickPatch.serverTimePerTick; + y = DrawStatusLine(x, y, width, "Server TPT:", $"{serverTpt:F1}ms", Color.white); + + // Average frame time + y = DrawStatusLine(x, y, width, "Avg Frame:", $"{TickPatch.avgFrameTime:F1}ms", Color.white); + + return y - startY; + } + + /// + /// Draw network and sync status section + /// + private static float DrawNetworkSyncSection(float x, float y, float width) + { + float startY = y; + + y = DrawSectionHeader(x, y, width, "NETWORK & SYNC"); + + if (Multiplayer.session != null) + { + // Player count + int playerCount = Multiplayer.session.players.Count; + bool hasDesynced = Multiplayer.session.players.Any(p => p.status == PlayerStatus.Desynced); + Color playerColor = hasDesynced ? Color.red : Color.green; + y = DrawStatusLine(x, y, width, "Players:", $"{playerCount}", playerColor); + + // Commands + y = DrawStatusLine(x, y, width, "Received Cmds:", $"{Multiplayer.session.receivedCmds}", Color.white); + y = DrawStatusLine(x, y, width, "Remote Sent:", $"{Multiplayer.session.remoteSentCmds}", Color.white); + y = DrawStatusLine(x, y, width, "Remote Tick:", $"{Multiplayer.session.remoteTickUntil}", Color.white); + + // Server status + string serverStatus = TickPatch.serverFrozen ? "Frozen" : "Running"; + Color serverColor = TickPatch.serverFrozen ? Color.yellow : Color.green; + y = DrawStatusLine(x, y, width, "Server:", serverStatus, serverColor); + } + else + { + y = DrawStatusLine(x, y, width, "Network:", "No active session", Color.gray); + } + + return y - startY; + } + + /// + /// Draw detailed debug section with existing comprehensive information + /// + private static float DrawDetailedDebugSection(float x, float y, float width) + { + float startY = y; + + y = DrawSectionHeader(x, y, width, "DETAILED DEBUG"); + + // Add existing comprehensive debug information here + // This preserves all the valuable debug data from IngameDebug.DoDebugPrintout + + if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) + { + var async = Find.CurrentMap.AsyncTime(); + + // Core debug information + y = DrawStatusLine(x, y, width, "Faction Stack:", $"{FactionContext.stack.Count}", Color.white); + y = DrawStatusLine(x, y, width, "Player Faction:", $"{Faction.OfPlayer.loadID}", Color.white); + y = DrawStatusLine(x, y, width, "Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID}", Color.white); + y = DrawStatusLine(x, y, width, "Next Thing ID:", $"{Find.UniqueIDsManager.nextThingID}", Color.white); + y = DrawStatusLine(x, y, width, "Next Job ID:", $"{Find.UniqueIDsManager.nextJobID}", Color.white); + y = DrawStatusLine(x, y, width, "Game Ticks:", $"{Find.TickManager.TicksGame}", Color.white); + y = DrawStatusLine(x, y, width, "Time Speed:", $"{Find.TickManager.CurTimeSpeed}", Color.white); + + // Timing information + int timerLag = TickPatch.tickUntil - TickPatch.Timer; + Color lagColor = timerLag > 30 ? Color.red : timerLag > 15 ? Color.yellow : Color.green; + y = DrawStatusLine(x, y, width, "Timer Lag:", $"{timerLag}", lagColor); + y = DrawStatusLine(x, y, width, "Timer:", $"{TickPatch.Timer}", Color.white); + y = DrawStatusLine(x, y, width, "Tick Until:", $"{TickPatch.tickUntil}", Color.white); + + // Error tracking + y = DrawStatusLine(x, y, width, "DST Errors:", $"{DeferredStackTracing.acc}", + DeferredStackTracing.acc > 0 ? Color.red : Color.green); + + // Memory and system info + y = DrawStatusLine(x, y, width, "Buffered Changes:", $"{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}", Color.white); + y = DrawStatusLine(x, y, width, "World Pawns:", $"{Find.WorldPawns.AllPawnsAliveOrDead.Count}", Color.white); + y = DrawStatusLine(x, y, width, "Pool Free Items:", $"{SimplePool.FreeItemsCount}", Color.white); + y = DrawStatusLine(x, y, width, "Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white); + y = DrawStatusLine(x, y, width, "Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white); + } + + return y - startY; + } + + // Helper methods for UI drawing + + private static void DrawStatusIcon(Rect rect, string icon, Color color, string tooltip) + { + try + { + // Draw background for debugging + Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.5f)); + + using (MpStyle.Set(color).Set(TextAnchor.MiddleCenter).Set(GameFont.Small)) + { + Widgets.Label(rect, icon); + } + + if (!string.IsNullOrEmpty(tooltip)) + { + TooltipHandler.TipRegion(rect, tooltip); + } + } + catch (Exception ex) + { + // Fallback: draw a simple colored rectangle + Widgets.DrawBoxSolid(rect, color); + Log.Warning($"DrawStatusIcon error: {ex.Message}"); + } + } + + private static float DrawSectionHeader(float x, float y, float width, string title) + { + Rect headerRect = new Rect(x, y, width, LineHeight + 2f); + using (MpStyle.Set(GameFont.Small).Set(Color.cyan).Set(TextAnchor.MiddleLeft)) + { + Widgets.Label(headerRect, $"── {title} ──"); + } + return y + LineHeight + 6f; // More spacing after headers + } + + private static float DrawStatusLine(float x, float y, float width, string label, string value, Color valueColor) + { + // Use consistent column widths and add padding + float labelWidth = width * LabelColumnWidth; + float valueWidth = width * (1f - LabelColumnWidth); + + // Label with right padding + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleLeft)) + { + Rect labelRect = new Rect(x, y, labelWidth - 4f, LineHeight); + Widgets.Label(labelRect, label); + } + + // Value with left padding + using (MpStyle.Set(GameFont.Tiny).Set(valueColor).Set(TextAnchor.MiddleLeft)) + { + Rect valueRect = new Rect(x + labelWidth + 4f, y, valueWidth - 4f, LineHeight); + Widgets.Label(valueRect, value); + } + + return y + LineHeight + 1f; // Small padding between lines + } + + // Status calculation methods + + private static (string icon, Color color, string text, string tooltip) GetSyncStatus() + { + try + { + if (Find.CurrentMap?.AsyncTime() != null && Multiplayer.AsyncWorldTime != null) + { + var async = Find.CurrentMap.AsyncTime(); + var worldAsync = Multiplayer.AsyncWorldTime; + bool inSync = async.randState == worldAsync.randState; + + return inSync + ? ("●", Color.green, "SYNC", "RNG states are synchronized") + : ("●", Color.red, "DESYNC", "RNG states are out of sync!"); + } + + return ("●", Color.yellow, "N/A", "No current map"); + } + catch (Exception ex) + { + Log.Warning($"GetSyncStatus error: {ex.Message}"); + return ("?", Color.gray, "ERR", "Error getting sync status"); + } + } + + private static (string icon, Color color, string text, string tooltip) GetPerformanceStatus() + { + try + { + float tps = IngameUIPatch.tps; + + if (tps > 40f) + return ("▲", Color.green, $"{tps:F1}", "Performance is good"); + else if (tps > 20f) + return ("▲", Color.yellow, $"{tps:F1}", "Performance is moderate"); + else + return ("▲", Color.red, $"{tps:F1}", "Performance is poor"); + } + catch (Exception ex) + { + Log.Warning($"GetPerformanceStatus error: {ex.Message}"); + return ("?", Color.gray, "ERR", "Error getting performance status"); + } + } + + private static (string icon, Color color, string text, string tooltip) GetErrorStatus() + { + try + { + int errors = DeferredStackTracing.acc; + + if (errors == 0) + return ("■", Color.green, "0", "No errors detected"); + else if (errors < 10) + return ("■", Color.yellow, $"{errors}", "Some errors detected"); + else + return ("■", Color.red, $"{errors}", "Many errors detected!"); + } + catch (Exception ex) + { + Log.Warning($"GetErrorStatus error: {ex.Message}"); + return ("?", Color.gray, "ERR", "Error getting error status"); + } + } + + private static (string icon, Color color, string text, string tooltip) GetTickStatus() + { + try + { + int behind = TickPatch.tickUntil - TickPatch.Timer; + + if (behind <= 5) + return ("♦", Color.green, $"{behind}", "Timing is good"); + else if (behind <= 15) + return ("♦", Color.yellow, $"{behind}", "Slightly behind"); + else + return ("♦", Color.red, $"{behind}", "Significantly behind"); + } + catch (Exception ex) + { + Log.Warning($"GetTickStatus error: {ex.Message}"); + return ("?", Color.gray, "ERR", "Error getting tick status"); + } + } + + // Utility methods + + private static float CalculateExpandedHeight() + { + return Math.Min(ExpandedMaxHeight, GetContentHeight() + 50f); + } + + private static float GetContentHeight() + { + // Calculate actual content height dynamically by counting lines + float height = 0f; + + // Status summary section + height += LineHeight + 6f; // Header + height += 4 * (LineHeight + 1f); // 4 status lines + height += SectionSpacing; + + // RNG states section + height += LineHeight + 6f; // Header + if (Find.CurrentMap?.AsyncTime() != null) + { + height += 3 * (LineHeight + 1f); // 3 RNG lines + } + else + { + height += 1 * (LineHeight + 1f); // 1 "No current map" line + } + height += SectionSpacing; + + // Performance section + height += LineHeight + 6f; // Header + height += 4 * (LineHeight + 1f); // 4 performance lines + height += SectionSpacing; + + // Network & sync section + height += LineHeight + 6f; // Header + if (Multiplayer.session != null) + { + height += 5 * (LineHeight + 1f); // 5 network lines + } + else + { + height += 1 * (LineHeight + 1f); // 1 "No active session" line + } + height += SectionSpacing; + + // Detailed debug section + height += LineHeight + 6f; // Header + if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) + { + height += 15 * (LineHeight + 1f); // Actual debug lines count + } + else + { + height += 1 * (LineHeight + 1f); // Minimal fallback + } + + // Small bottom padding to prevent cutoff + height += 20f; + + return height; + } + } +} \ No newline at end of file From c935ca0424e8b770cbd182d3004a0289e2049a29 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 6 Jul 2025 11:20:48 +1000 Subject: [PATCH 11/38] updated rbp for mac --- Source/Common/DeferredStackTracingImpl.cs | 34 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index 56fe3dc2..fa3f9317 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -1,5 +1,7 @@ -using System; +using System; using System.Runtime.CompilerServices; +using UnityEngine; +using Verse; namespace Multiplayer.Client.Desyncs; @@ -34,10 +36,27 @@ struct AddrInfo public const int MaxDepth = 32; public const int HashInfluence = 6; + private static unsafe delegate* getRbpFunc; + + static unsafe DeferredStackTracingImpl() + { + getRbpFunc = Application.platform switch + { + RuntimePlatform.LinuxEditor => &GetRbpWindows, + RuntimePlatform.LinuxPlayer => &GetRbpWindows, + RuntimePlatform.OSXEditor => &GetRbpMac, + RuntimePlatform.OSXPlayer => &GetRbpMac, + _ => &GetRbpWindows + }; + } + public static unsafe int TraceImpl(long[] traceIn, ref int hash) { + if (Native.LmfPtr == 0) + return 0; + long[] trace = traceIn; - long rbp = GetRbp(); + long rbp = getRbpFunc(); long stck = rbp; rbp = *(long*)rbp; @@ -235,10 +254,17 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage) } [MethodImpl(MethodImplOptions.NoInlining)] - static unsafe long GetRbp() + static unsafe long GetRbpMac() + { + long rbp = 0; + return *(&rbp + 1); // Mac offset + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static unsafe long GetRbpWindows() { long rbp = 0; - return *(&rbp + 4); // Use offset 4 which works consistently in RimWorld 1.6 + return *(&rbp + 4); // Windows offset } public static int HashCombineInt(int seed, int value) From c864bf0f6b8e6e22a84eb1b85c107ab913a7b4e5 Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 9 Jul 2025 20:02:47 +1000 Subject: [PATCH 12/38] - Returned InGameDebug to its original state (I think?) - Improved SyncDebugPanel --- Source/Client/UI/IngameDebug.cs | 29 +++++- Source/Client/UI/SyncDebugPanel.cs | 159 ++++++++++++++++++++--------- 2 files changed, 133 insertions(+), 55 deletions(-) diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index 7e498230..72a6bc5d 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -103,12 +103,31 @@ internal static void DoDebugPrintout() // RandGetValuePatch.tracesThistick = 0; } - // Original comprehensive debug information method - // Now replaced by SyncDebugPanel.DoSyncDebugPanel for enhanced UI/UX - // DoDebugPrintout() disabled to prevent overlap with new enhanced panel - public static float DoDevInfo(float y) + + internal static float DoDevInfo(float y) { - // Legacy debug display disabled - now handled by SyncDebugPanel + float x = UI.screenWidth - BtnWidth - BtnMargin; + + if (Multiplayer.ShowDevInfo && Multiplayer.WriterLog != null) + { + if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Write ({Multiplayer.WriterLog.NodeCount})")) + Find.WindowStack.Add(Multiplayer.WriterLog); + + y += BtnHeight; + if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Read ({Multiplayer.ReaderLog.NodeCount})")) + Find.WindowStack.Add(Multiplayer.ReaderLog); + + y += BtnHeight; + var oldGhostMode = Multiplayer.session.ghostModeCheckbox; + Widgets.CheckboxLabeled(new Rect(x, y, BtnWidth, 30f), "Ghost", ref Multiplayer.session.ghostModeCheckbox); + if (oldGhostMode != Multiplayer.session.ghostModeCheckbox) + { + SyncFieldUtil.ClearAllBufferedChanges(); + } + + return BtnHeight * 3; + } + return 0; } diff --git a/Source/Client/UI/SyncDebugPanel.cs b/Source/Client/UI/SyncDebugPanel.cs index eb93edfe..a74343ec 100644 --- a/Source/Client/UI/SyncDebugPanel.cs +++ b/Source/Client/UI/SyncDebugPanel.cs @@ -37,8 +37,8 @@ public static class SyncDebugPanel public static float DoSyncDebugPanel(float y) { // Safety checks - if (Multiplayer.session == null) return 0; - + if (Multiplayer.session == null || !MpVersion.IsDebug || !Multiplayer.ShowDevInfo || Multiplayer.WriterLog == null) return 0; + try { float x = Margin; @@ -96,7 +96,7 @@ private static void DrawCompactPanel(Rect rect) currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); currentX += 4f; - // Error status [📊 0] + // RNG status [📊 0] currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); currentX += 4f; @@ -240,8 +240,8 @@ private static void DrawPanelHeader(Rect rect) currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); currentX += 4f; - // Error status [■ 0] - currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); + // RNG status [■ 0] + currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); currentX += 4f; // Tick status [♦ 3] @@ -299,7 +299,7 @@ private static float DrawStatusSummarySection(float x, float y, float width) // Status lines y = DrawStatusLine(x, y, width, "Sync Status:", syncStatus.text, syncStatus.color); y = DrawStatusLine(x, y, width, "Performance:", performanceStatus.text, performanceStatus.color); - y = DrawStatusLine(x, y, width, "Error Rate:", errorStatus.text, errorStatus.color); + y = DrawStatusLine(x, y, width, "Status:", errorStatus.text, errorStatus.color); y = DrawStatusLine(x, y, width, "Tick Status:", tickStatus.text, tickStatus.color); return y - startY; @@ -319,21 +319,19 @@ private static float DrawRNGStatesSection(float x, float y, float width) var async = Find.CurrentMap.AsyncTime(); var worldAsync = Multiplayer.AsyncWorldTime; - // Map RNG state + // Current Map RNG state string mapRngLow = $"{(uint)async.randState:X8}"; string mapRngHigh = $"{(uint)(async.randState >> 32):X8}"; - y = DrawStatusLine(x, y, width, "Map RNG:", $"{mapRngHigh} | {mapRngLow}", Color.white); + y = DrawStatusLine(x, y, width, "Current Map:", $"{mapRngHigh} | {mapRngLow}", Color.white); // World RNG state string worldRngLow = $"{(uint)worldAsync.randState:X8}"; string worldRngHigh = $"{(uint)(worldAsync.randState >> 32):X8}"; y = DrawStatusLine(x, y, width, "World RNG:", $"{worldRngHigh} | {worldRngLow}", Color.white); - // Sync indicator - bool rngInSync = async.randState == worldAsync.randState; - Color syncColor = rngInSync ? Color.green : Color.red; - string syncText = rngInSync ? "✓ In Sync" : "✗ Desync Detected"; - y = DrawStatusLine(x, y, width, "RNG Sync:", syncText, syncColor); + // Round Mode + var roundMode = RoundMode.GetCurrentRoundMode(); + y = DrawStatusLine(x, y, width, "Round Mode:", $"{roundMode}", Color.white); } else { @@ -414,16 +412,12 @@ private static float DrawDetailedDebugSection(float x, float y, float width) { float startY = y; - y = DrawSectionHeader(x, y, width, "DETAILED DEBUG"); - - // Add existing comprehensive debug information here - // This preserves all the valuable debug data from IngameDebug.DoDebugPrintout - if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) { var async = Find.CurrentMap.AsyncTime(); - // Core debug information + // ===== CORE SYSTEM DATA ===== + y = DrawSectionHeader(x, y, width, "CORE SYSTEM"); y = DrawStatusLine(x, y, width, "Faction Stack:", $"{FactionContext.stack.Count}", Color.white); y = DrawStatusLine(x, y, width, "Player Faction:", $"{Faction.OfPlayer.loadID}", Color.white); y = DrawStatusLine(x, y, width, "Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID}", Color.white); @@ -431,24 +425,78 @@ private static float DrawDetailedDebugSection(float x, float y, float width) y = DrawStatusLine(x, y, width, "Next Job ID:", $"{Find.UniqueIDsManager.nextJobID}", Color.white); y = DrawStatusLine(x, y, width, "Game Ticks:", $"{Find.TickManager.TicksGame}", Color.white); y = DrawStatusLine(x, y, width, "Time Speed:", $"{Find.TickManager.CurTimeSpeed}", Color.white); + y += SectionSpacing; - // Timing information + // ===== TIMING & SYNC DATA ===== + y = DrawSectionHeader(x, y, width, "TIMING & SYNC"); int timerLag = TickPatch.tickUntil - TickPatch.Timer; Color lagColor = timerLag > 30 ? Color.red : timerLag > 15 ? Color.yellow : Color.green; y = DrawStatusLine(x, y, width, "Timer Lag:", $"{timerLag}", lagColor); y = DrawStatusLine(x, y, width, "Timer:", $"{TickPatch.Timer}", Color.white); y = DrawStatusLine(x, y, width, "Tick Until:", $"{TickPatch.tickUntil}", Color.white); - - // Error tracking - y = DrawStatusLine(x, y, width, "DST Errors:", $"{DeferredStackTracing.acc}", - DeferredStackTracing.acc > 0 ? Color.red : Color.green); - - // Memory and system info + y = DrawStatusLine(x, y, width, "Raw Tick Timer:", $"{TickPatch.tickTimer.ElapsedMilliseconds}ms", Color.white); + y = DrawStatusLine(x, y, width, "World Settlements:", $"{Find.World.worldObjects.settlements.Count}", Color.white); + y += SectionSpacing; + + // ===== GAME STATE DATA ===== + y = DrawSectionHeader(x, y, width, "GAME STATE"); + y = DrawStatusLine(x, y, width, "Classic Mode:", $"{Find.IdeoManager.classicMode}", Color.white); + y = DrawStatusLine(x, y, width, "Client Opinions:", $"{Multiplayer.game.sync.knownClientOpinions.Count}", Color.white); + y = DrawStatusLine(x, y, width, "First Opinion Tick:", $"{Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick}", Color.white); + y = DrawStatusLine(x, y, width, "Map Ticks:", $"{async.mapTicks}", Color.white); + y = DrawStatusLine(x, y, width, "Frozen At:", $"{TickPatch.frozenAt}", Color.white); + y += SectionSpacing; + + // ===== RNG & DEBUG DATA ===== + y = DrawSectionHeader(x, y, width, "RNG & DEBUG"); + y = DrawStatusLine(x, y, width, "Rand Calls:", $"{DeferredStackTracing.acc}", Color.white); + y = DrawStatusLine(x, y, width, "Max Trace Depth:", $"{DeferredStackTracing.maxTraceDepth}", Color.white); + y = DrawStatusLine(x, y, width, "Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white); + y = DrawStatusLine(x, y, width, "Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white); + y += SectionSpacing; + + // ===== COMMAND & SYNC DATA ===== + y = DrawSectionHeader(x, y, width, "COMMAND & SYNC"); + y = DrawStatusLine(x, y, width, "Async Commands:", $"{async.cmds.Count}", Color.white); + y = DrawStatusLine(x, y, width, "World Commands:", $"{Multiplayer.AsyncWorldTime.cmds.Count}", Color.white); + y = DrawStatusLine(x, y, width, "Force Normal Speed:", $"{async.slower.forceNormalSpeedUntil}", Color.white); + y = DrawStatusLine(x, y, width, "Async Time Status:", $"{Multiplayer.GameComp.asyncTime}", Color.white); y = DrawStatusLine(x, y, width, "Buffered Changes:", $"{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}", Color.white); + y += SectionSpacing; + + // ===== MEMORY & PERFORMANCE DATA ===== + y = DrawSectionHeader(x, y, width, "MEMORY & PERFORMANCE"); y = DrawStatusLine(x, y, width, "World Pawns:", $"{Find.WorldPawns.AllPawnsAliveOrDead.Count}", Color.white); y = DrawStatusLine(x, y, width, "Pool Free Items:", $"{SimplePool.FreeItemsCount}", Color.white); - y = DrawStatusLine(x, y, width, "Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white); - y = DrawStatusLine(x, y, width, "Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white); + + // Calculated server performance + var calcStpt = TickPatch.tickUntil - TickPatch.Timer <= 3 ? TickPatch.serverTimePerTick * 1.2f : + TickPatch.tickUntil - TickPatch.Timer >= 7 ? TickPatch.serverTimePerTick * 0.8f : + TickPatch.serverTimePerTick; + y = DrawStatusLine(x, y, width, "Calc Server TPT:", $"{calcStpt:F1}ms", Color.white); + y += SectionSpacing; + + // ===== MAP MANAGEMENT DATA ===== + y = DrawSectionHeader(x, y, width, "MAP MANAGEMENT"); + y = DrawStatusLine(x, y, width, "Haul Destinations:", $"{Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count}", Color.white); + y = DrawStatusLine(x, y, width, "Designations:", $"{Find.CurrentMap.designationManager.designationsByDef.Count}", Color.white); + y = DrawStatusLine(x, y, width, "Haulable Items:", $"{Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white); + y = DrawStatusLine(x, y, width, "Mining Designations:", $"{Find.CurrentMap.designationManager.SpawnedDesignationsOfDef(DesignationDefOf.Mine).Count()}", Color.white); + y = DrawStatusLine(x, y, width, "First Ideology ID:", $"{Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}", Color.white); + + // Faction-specific data (if available) + if (Find.CurrentMap.ParentFaction != null) + { + int faction = Find.CurrentMap.ParentFaction.loadID; + MultiplayerMapComp comp = Find.CurrentMap.MpComp(); + FactionMapData data = comp.factionData.TryGetValue(faction); + + if (data != null) + { + y = DrawStatusLine(x, y, width, "Faction Haulables:", $"{data.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white); + y = DrawStatusLine(x, y, width, "Faction Haul Groups:", $"{data.haulDestinationManager.AllGroupsListForReading.Count}", Color.white); + } + } } return y - startY; @@ -520,18 +568,22 @@ private static (string icon, Color color, string text, string tooltip) GetSyncSt { try { - if (Find.CurrentMap?.AsyncTime() != null && Multiplayer.AsyncWorldTime != null) + // Check if there's an active multiplayer session and desync detection + if (Multiplayer.session != null) { - var async = Find.CurrentMap.AsyncTime(); - var worldAsync = Multiplayer.AsyncWorldTime; - bool inSync = async.randState == worldAsync.randState; + bool hasDesynced = Multiplayer.session.players.Any(p => p.status == PlayerStatus.Desynced); - return inSync - ? ("●", Color.green, "SYNC", "RNG states are synchronized") - : ("●", Color.red, "DESYNC", "RNG states are out of sync!"); + if (hasDesynced) + return ("●", Color.red, "DESYNC", "Players have desynced!"); + + // Check if we're in a valid sync state + if (Multiplayer.session.desynced) + return ("●", Color.red, "DESYNC", "Session has desynced"); + + return ("●", Color.green, "SYNC", "All players are synchronized"); } - return ("●", Color.yellow, "N/A", "No current map"); + return ("●", Color.yellow, "N/A", "No active session"); } catch (Exception ex) { @@ -564,19 +616,13 @@ private static (string icon, Color color, string text, string tooltip) GetErrorS { try { - int errors = DeferredStackTracing.acc; - - if (errors == 0) - return ("■", Color.green, "0", "No errors detected"); - else if (errors < 10) - return ("■", Color.yellow, $"{errors}", "Some errors detected"); - else - return ("■", Color.red, $"{errors}", "Many errors detected!"); + // For now, return a placeholder - we can add a more useful metric here later + return ("■", Color.gray, "N/A", "No critical metric"); } catch (Exception ex) { Log.Warning($"GetErrorStatus error: {ex.Message}"); - return ("?", Color.gray, "ERR", "Error getting error status"); + return ("?", Color.gray, "ERR", "Error getting status"); } } @@ -621,7 +667,8 @@ private static float GetContentHeight() height += LineHeight + 6f; // Header if (Find.CurrentMap?.AsyncTime() != null) { - height += 3 * (LineHeight + 1f); // 3 RNG lines + // Base RNG lines: Current Map, World, Round Mode + height += 3 * (LineHeight + 1f); } else { @@ -646,11 +693,23 @@ private static float GetContentHeight() } height += SectionSpacing; - // Detailed debug section - height += LineHeight + 6f; // Header + // Detailed debug section (organized into 6 subsections) if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) { - height += 15 * (LineHeight + 1f); // Actual debug lines count + // Core System: 7 lines + header + height += LineHeight + 6f + 7 * (LineHeight + 1f) + SectionSpacing; + // Timing & Sync: 5 lines + header + height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; + // Game State: 5 lines + header + height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; + // Map Management: 5-7 lines + header (including faction data) - moved to bottom + height += LineHeight + 6f + 7 * (LineHeight + 1f); + // RNG & Debug: 4 lines + header + height += LineHeight + 6f + 4 * (LineHeight + 1f) + SectionSpacing; + // Command & Sync: 5 lines + header + height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; + // Memory & Performance: 3 lines + header + height += LineHeight + 6f + 3 * (LineHeight + 1f); } else { @@ -663,4 +722,4 @@ private static float GetContentHeight() return height; } } -} \ No newline at end of file +} From 720e7b390029ea2ce496bc427ce9e918569674a4 Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 9 Jul 2025 20:25:18 +1000 Subject: [PATCH 13/38] Renamed IsMutant to IsSubhuman --- Source/Client/Factions/MultifactionPatches.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 96e580b8..c2b1cab0 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -235,9 +235,9 @@ public static bool IsColonyMechAnyFaction(Pawn p) return false; } - public static bool IsColonyMutantAnyFaction(Pawn p) + public static bool IsColonySubhuman(Pawn p) { - return ModsConfig.AnomalyActive && p.IsMutant && p.Faction is { IsPlayer: true }; + return ModsConfig.AnomalyActive && p.IsSubhuman && p.Faction is { IsPlayer: true }; } } @@ -296,7 +296,7 @@ static IEnumerable Transpiler(IEnumerable inst inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); if (inst.operand == isColonySubhuman) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonyMutantAnyFaction)); + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhuman)); yield return inst; } From 7dbb387355b049f1665e8a8a4499ad68376f9b14 Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 9 Jul 2025 20:30:46 +1000 Subject: [PATCH 14/38] Removed last instance of int tile --- Source/Client/UI/LocationPings.cs | 7 ++++--- Source/Common/Networking/State/ServerPlayingState.cs | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Client/UI/LocationPings.cs b/Source/Client/UI/LocationPings.cs index a78944e2..67c85e6d 100644 --- a/Source/Client/UI/LocationPings.cs +++ b/Source/Client/UI/LocationPings.cs @@ -58,11 +58,12 @@ private static bool KeyDown(KeyCode? keyNullable) return Input.GetKeyDown(key); } - private void PingLocation(int map, int tile, Vector3 loc) + private void PingLocation(int map, PlanetTile tile, Vector3 loc) { var writer = new ByteWriter(); writer.WriteInt32(map); - writer.WriteInt32(tile); + writer.WriteInt32(tile.tileId); + writer.WriteInt32(tile.layerId); writer.WriteFloat(loc.x); writer.WriteFloat(loc.y); writer.WriteFloat(loc.z); @@ -71,7 +72,7 @@ private void PingLocation(int map, int tile, Vector3 loc) SoundDefOf.TinyBell.PlayOneShotOnCamera(); } - public void ReceivePing(int player, int map, int tile, Vector3 loc) + public void ReceivePing(int player, int map, PlanetTile tile, Vector3 loc) { if (!Multiplayer.settings.enablePings) return; diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index d12da74a..6f4c6ce2 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -168,7 +168,8 @@ public void HandlePing(ByteReader data) writer.WriteInt32(Player.id); writer.WriteInt32(data.ReadInt32()); // Map id - writer.WriteInt32(data.ReadInt32()); // Planet tile + writer.WriteInt32(data.ReadInt32()); // Planet tile id + writer.WriteInt32(data.ReadInt32()); // Planet tile layer writer.WriteFloat(data.ReadFloat()); // X writer.WriteFloat(data.ReadFloat()); // Y writer.WriteFloat(data.ReadFloat()); // Z From 658e98108970a72baaa6524dba54d673713fb04b Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 9 Jul 2025 20:32:28 +1000 Subject: [PATCH 15/38] Correctly named function IsColonySubhumanAnyFaction --- Source/Client/Factions/MultifactionPatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index c2b1cab0..239e4c0c 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -235,7 +235,7 @@ public static bool IsColonyMechAnyFaction(Pawn p) return false; } - public static bool IsColonySubhuman(Pawn p) + public static bool IsColonySubhumanAnyFaction(Pawn p) { return ModsConfig.AnomalyActive && p.IsSubhuman && p.Faction is { IsPlayer: true }; } @@ -296,7 +296,7 @@ static IEnumerable Transpiler(IEnumerable inst inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonistAnyFaction)); if (inst.operand == isColonySubhuman) - inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhuman)); + inst.operand = AccessTools.Method(typeof(RecacheColonistBelieverCountPatch), nameof(RecacheColonistBelieverCountPatch.IsColonySubhumanAnyFaction)); yield return inst; } From 3897a17d223ceb9f16e5e908d1fcde921fbea4c3 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 13:54:05 +1000 Subject: [PATCH 16/38] Added MP log files --- Source/Client/Multiplayer.cs | 2 + Source/Client/Util/MpLog.cs | 1 + Source/Client/Util/SaveableMpLogs.cs | 131 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 Source/Client/Util/SaveableMpLogs.cs diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 628aa981..8c5efafd 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -65,6 +65,7 @@ public static class Multiplayer public static string ReplaysDir => GenFilePaths.FolderUnderSaveData("MpReplays"); public static string DesyncsDir => GenFilePaths.FolderUnderSaveData("MpDesyncs"); + public static string MpLogsDir => GenFilePaths.FolderUnderSaveData("MpLogs"); public static Stopwatch clock = Stopwatch.StartNew(); @@ -186,6 +187,7 @@ public static void StopMultiplayerAndClearAllWindows() public static void StopMultiplayer() { Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); + SaveableMpLogs.ResetMpLogs(); OnMainThread.ClearScheduled(); LongEventHandler.ClearQueuedEvents(); diff --git a/Source/Client/Util/MpLog.cs b/Source/Client/Util/MpLog.cs index 4045ab3c..7802dcfe 100644 --- a/Source/Client/Util/MpLog.cs +++ b/Source/Client/Util/MpLog.cs @@ -23,6 +23,7 @@ public static void Error(string msg) public static void Debug(string msg) { Verse.Log.Message($"{Multiplayer.username} {TickPatch.Timer} {msg}"); + SaveableMpLogs.AddLog(msg); } } } diff --git a/Source/Client/Util/SaveableMpLogs.cs b/Source/Client/Util/SaveableMpLogs.cs new file mode 100644 index 00000000..971530f2 --- /dev/null +++ b/Source/Client/Util/SaveableMpLogs.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using HarmonyLib; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Util; + +public class SaveableMpLogs +{ + private const int MaxFiles = 10; + private const string FilePrefix = "MpLog-"; + private const string FileExtension = ".log"; + + private static string _currentLogFile = null; + + public static void InitMpLogs() + { + _currentLogFile = FindFileNameForNextFile(); + + try + { + using var stream = File.Open(_currentLogFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine(GetLogDetails()); + } + catch (Exception e) + { + Log.Error($"Exception writing initial log info: {e}"); + } + } + + public static void ResetMpLogs() => _currentLogFile = null; + + public static void AddLog(string logText) + { + Log.Message($"MpLog: {logText}"); + // Don't log if not connected + if (Multiplayer.Client == null) + { + return; + } + + if (_currentLogFile == null) + { + InitMpLogs(); + } + + int ticks = Find.TickManager.ticksGameInt; + int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; + + try + { + using var stream = File.Open(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine($"[{ticks}] [{mapTicks}] {logText}"); + } + catch (Exception e) + { + Log.Error($"Exception writing log info: {e}"); + } + } + + private static string GetLogDetails() + { + var logDetails = new StringBuilder() + .AppendLine($"Multiplayer Log - {DateTime.Now}") + .AppendLine("\n###Version Data###") + .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") + .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") + .AppendLine("\n###Debug Options###") + .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") + .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") + .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") + .AppendLine("\n###Server Info###") + .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") + .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") + .AppendLine("\n###OS Info###") + .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") + .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}") + .AppendLine("======================================================") + .AppendLine("###Log Start###") + .AppendLine("======================================================"); + return logDetails.ToString(); + } + + private static string FindFileNameForNextFile() + { + // Get player directory + string directory = Path.Combine(Multiplayer.MpLogsDir, Multiplayer.username); + + // Ensure the directory exists + Directory.CreateDirectory(directory); + + // Get all existing logs + FileInfo[] files = new DirectoryInfo(directory).GetFiles($"{FilePrefix}*{FileExtension}"); + + // Delete any pushing us over the limit, and reserve room for one more + if (files.Length > MaxFiles - 1) + files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); + + // Find the current max number + int max = 0; + foreach (FileInfo file in files) + { + // Get name without extension and prefix + string parsedName = Path.GetFileNameWithoutExtension(file.Name)[FilePrefix.Length..]; + + // Try to parse the number and update max if it's greater + if (int.TryParse(parsedName, out int result) && result > max) + max = result; + } + + return Path.Combine(directory, $"{FilePrefix}{max + 1:00}{FileExtension}"); + } + + private static void DeleteFileSilent(FileInfo file) + { + try + { + file.Delete(); + } + catch (IOException) + { + } + } +} From 062e847f2227878dcfca04fa5c681d71fe59a657 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 21:15:01 +1000 Subject: [PATCH 17/38] - Added Variable Tick Rate handling - Added player count to AsyncTimeComp - VTR is 1 if players are on the map, 15 otherwise. - Changes to Saveable MP Logs --- Source/Client/AsyncTime/AsyncTimeComp.cs | 7 +++++++ Source/Client/AsyncTime/AsyncWorldTimeComp.cs | 13 +++++++++++++ Source/Client/Util/SaveableMpLogs.cs | 10 ++-------- Source/Common/CommandType.cs | 1 + 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 837d28f1..f5a96ad1 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -10,6 +10,7 @@ using Multiplayer.Client.Patches; using Multiplayer.Client.Saving; using Multiplayer.Client.Util; +using System.Linq; namespace Multiplayer.Client { @@ -84,6 +85,9 @@ public void SetDesiredTimeSpeed(TimeSpeed speed) public Queue cmds = new(); + public int CurrentPlayerCount { get; private set; } = 0; + public int VTR => CurrentPlayerCount > 0 ? 1 : 15; + public AsyncTimeComp(Map map) { this.map = map; @@ -207,6 +211,9 @@ public void ExposeData() Scribe_Custom.LookULong(ref randState, "randState", 1); } + public void IncreasePlayerCount() => CurrentPlayerCount++; + public void DecreasePlayerCount() => CurrentPlayerCount = Math.Max(0, CurrentPlayerCount - 1); + public void FinalizeInit() { cmds = new Queue( diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs index 6859309e..51c95432 100644 --- a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -219,6 +219,19 @@ public void ExecuteCmd(ScheduledCommand cmd) var canUseDevMode = data.ReadBool(); Multiplayer.GameComp.playerData[playerId] = new PlayerData { canUseDevMode = canUseDevMode }; } + + if (cmdType == CommandType.PlayerCount) + { + int previousMapId = data.ReadInt32(); + int newMapId = data.ReadInt32(); + int mapCount = Find.Maps.Count; + + if (0 <= previousMapId && previousMapId < mapCount) + Find.Maps[previousMapId]?.AsyncTime()?.DecreasePlayerCount(); + + if (0 <= newMapId && newMapId < mapCount) + Find.Maps[newMapId]?.AsyncTime()?.IncreasePlayerCount(); + } } catch (Exception e) { diff --git a/Source/Client/Util/SaveableMpLogs.cs b/Source/Client/Util/SaveableMpLogs.cs index 971530f2..69a838c9 100644 --- a/Source/Client/Util/SaveableMpLogs.cs +++ b/Source/Client/Util/SaveableMpLogs.cs @@ -38,17 +38,11 @@ public static void InitMpLogs() public static void AddLog(string logText) { - Log.Message($"MpLog: {logText}"); - // Don't log if not connected if (Multiplayer.Client == null) - { return; - } if (_currentLogFile == null) - { InitMpLogs(); - } int ticks = Find.TickManager.ticksGameInt; int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; @@ -82,7 +76,7 @@ private static string GetLogDetails() .AppendLine("\n###OS Info###") .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}") - .AppendLine("======================================================") + .AppendLine("\n======================================================") .AppendLine("###Log Start###") .AppendLine("======================================================"); return logDetails.ToString(); @@ -91,7 +85,7 @@ private static string GetLogDetails() private static string FindFileNameForNextFile() { // Get player directory - string directory = Path.Combine(Multiplayer.MpLogsDir, Multiplayer.username); + string directory = Path.Combine(Multiplayer.MpLogsDir);//, Multiplayer.username); // Ensure the directory exists Directory.CreateDirectory(directory); diff --git a/Source/Common/CommandType.cs b/Source/Common/CommandType.cs index ba3ce05d..0239db07 100644 --- a/Source/Common/CommandType.cs +++ b/Source/Common/CommandType.cs @@ -16,4 +16,5 @@ public enum CommandType : byte // Map scope MapTimeSpeed, Designator, + PlayerCount, } From ab1aaf3f2c7c9979a12e41611436aa912d6e4bc8 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 21:20:28 +1000 Subject: [PATCH 18/38] I missed a pretty critical file heh --- Source/Client/Patches/VTRSyncPatch.cs | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Source/Client/Patches/VTRSyncPatch.cs diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs new file mode 100644 index 00000000..43161a82 --- /dev/null +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Patches +{ + [HarmonyPatch(typeof(GenTicks), nameof(GenTicks.GetCameraUpdateRate))] + public static class VTRSyncPatch + { + static bool Prefix(Thing thing, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = GetSynchronizedUpdateRate(thing); + return false; + } + + private static int GetSynchronizedUpdateRate(Thing thing) => thing?.MapHeld?.AsyncTime()?.VTR ?? 15; + } + + [HarmonyPatch(typeof(Game), nameof(Game.CurrentMap), MethodType.Setter)] + static class MapSwitchPatch + { + private static int lastSentFromMap = int.MaxValue; + + static void Prefix(Map value) + { + if (Multiplayer.Client == null) return; + + try + { + int previousMap = Find.CurrentMap?.uniqueID ?? -1; + int newMap = value?.uniqueID ?? -1; + + // Only send when multiplayer is ready + if (Multiplayer.Client == null || Client.Multiplayer.session == null) + return; + + // If no change in map, do nothing + if (previousMap == newMap) + return; + + // Prevent duplicate commands for the same transition, but allow retry after a tick + if (lastSentFromMap == previousMap) + return; + + // Send map change command to server + // Send as global command since it affects multiple maps + Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(previousMap, newMap)); + + // Track this command to prevent duplicates + lastSentFromMap = previousMap; + } + catch (Exception ex) + { + MpLog.Error($"VTR MapSwitchPatch error: {ex.Message}"); + } + } + } +} From 663367cfdfa1785023326e495791dd2d7382c762 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 21:27:09 +1000 Subject: [PATCH 19/38] Updated VTR Synch patch --- Source/Client/Patches/VTRSyncPatch.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index 43161a82..a9fe1269 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -29,6 +29,7 @@ static bool Prefix(Thing thing, ref int __result) static class MapSwitchPatch { private static int lastSentFromMap = int.MaxValue; + private static int lastSentTick = -1; static void Prefix(Map value) { @@ -48,7 +49,7 @@ static void Prefix(Map value) return; // Prevent duplicate commands for the same transition, but allow retry after a tick - if (lastSentFromMap == previousMap) + if (lastSentFromMap == previousMap && Find.TickManager?.TicksGame == lastSentTick) return; // Send map change command to server @@ -57,6 +58,7 @@ static void Prefix(Map value) // Track this command to prevent duplicates lastSentFromMap = previousMap; + lastSentTick = Find.TickManager?.TicksGame ?? 0; } catch (Exception ex) { From 052525ccb4d84a48db22b667513c209120a368b6 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 21:31:51 +1000 Subject: [PATCH 20/38] Small change to vtr --- Source/Client/Patches/VTRSyncPatch.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index a9fe1269..5f76ef97 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -39,6 +39,7 @@ static void Prefix(Map value) { int previousMap = Find.CurrentMap?.uniqueID ?? -1; int newMap = value?.uniqueID ?? -1; + int currentTick = Find.TickManager?.TicksGame ?? 0; // Only send when multiplayer is ready if (Multiplayer.Client == null || Client.Multiplayer.session == null) @@ -49,7 +50,7 @@ static void Prefix(Map value) return; // Prevent duplicate commands for the same transition, but allow retry after a tick - if (lastSentFromMap == previousMap && Find.TickManager?.TicksGame == lastSentTick) + if (lastSentFromMap == previousMap && currentTick == lastSentTick) return; // Send map change command to server @@ -58,7 +59,7 @@ static void Prefix(Map value) // Track this command to prevent duplicates lastSentFromMap = previousMap; - lastSentTick = Find.TickManager?.TicksGame ?? 0; + lastSentTick = currentTick; } catch (Exception ex) { From dcf043f4535d05f2c26ac01f068f5250532db0b8 Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 21:36:22 +1000 Subject: [PATCH 21/38] Updated Debug Panel - Cleaned everything up --- Source/Client/UI/DebugPanel/DebugLine.cs | 22 + Source/Client/UI/DebugPanel/DebugSection.cs | 17 + Source/Client/UI/DebugPanel/StatusBadge.cs | 69 ++ Source/Client/UI/DebugPanel/SyncDebugPanel.cs | 479 ++++++++++++ Source/Client/UI/SyncDebugPanel.cs | 725 ------------------ 5 files changed, 587 insertions(+), 725 deletions(-) create mode 100644 Source/Client/UI/DebugPanel/DebugLine.cs create mode 100644 Source/Client/UI/DebugPanel/DebugSection.cs create mode 100644 Source/Client/UI/DebugPanel/StatusBadge.cs create mode 100644 Source/Client/UI/DebugPanel/SyncDebugPanel.cs delete mode 100644 Source/Client/UI/SyncDebugPanel.cs diff --git a/Source/Client/UI/DebugPanel/DebugLine.cs b/Source/Client/UI/DebugPanel/DebugLine.cs new file mode 100644 index 00000000..ac410fd4 --- /dev/null +++ b/Source/Client/UI/DebugPanel/DebugLine.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace Multiplayer.Client.DebugUi +{ +public static partial class SyncDebugPanel + { + // Data structures for organizing debug sections + private struct DebugLine + { + public string Label; + public string Value; + public Color Color; + + public DebugLine(string label, string value, Color color) + { + Label = label; + Value = value; + Color = color; + } + } + } +} diff --git a/Source/Client/UI/DebugPanel/DebugSection.cs b/Source/Client/UI/DebugPanel/DebugSection.cs new file mode 100644 index 00000000..659fe36e --- /dev/null +++ b/Source/Client/UI/DebugPanel/DebugSection.cs @@ -0,0 +1,17 @@ +namespace Multiplayer.Client.DebugUi +{ +public static partial class SyncDebugPanel + { + private struct DebugSection + { + public string Title; + public DebugLine[] Lines; + + public DebugSection(string title, DebugLine[] lines) + { + Title = title; + Lines = lines; + } + } + } +} diff --git a/Source/Client/UI/DebugPanel/StatusBadge.cs b/Source/Client/UI/DebugPanel/StatusBadge.cs new file mode 100644 index 00000000..8c54f1a4 --- /dev/null +++ b/Source/Client/UI/DebugPanel/StatusBadge.cs @@ -0,0 +1,69 @@ +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.DebugUi +{ + public struct StatusBadge + { + public string icon; + public Color color; + public string text; + public string tooltip; + + public StatusBadge(string icon, Color color, string text, string tooltip) + { + this.icon = icon; + this.color = color; + this.text = text; + this.tooltip = tooltip; + } + + public static StatusBadge GetSyncStatus() + { + if (Multiplayer.session == null) + return new StatusBadge("●", Color.yellow, "N/A", "No active session"); + + if (Multiplayer.session.desynced) + return new StatusBadge("●", Color.red, "DESYNC", "Session has desynced"); + + return new StatusBadge("●", Color.green, "SYNC", "All players are synchronized"); + } + + public static StatusBadge GetPerformanceStatus() + { + float tps = IngameUIPatch.tps; + string tooltip = tps > 40f ? "Performance is good" : tps > 20f ? "Performance is moderate" : "Performance is poor"; + + return new StatusBadge("▲", GetPerformanceColor(tps, 40f, 20f), $"{tps:F1}", tooltip); + } + + public static StatusBadge GetTickStatus() + { + int behind = TickPatch.tickUntil - TickPatch.Timer; + Color color = GetPerformanceColor(behind, 5, 15, true); + string tooltip = behind <= 5 ? "Timing is good" : behind <= 15 ? "Slightly behind" : "Significantly behind"; + return new StatusBadge("♦", color, behind.ToString(), tooltip); + } + + public static StatusBadge GetVtrStatus() + { + int rate = Find.CurrentMap?.AsyncTime()?.VTR ?? 15; + return new StatusBadge("V", rate == 15 ? Color.red : Color.green, rate.ToString(), $"Variable Tick Rate: Things update every {rate} tick(s)"); + } + + public static StatusBadge GetNumOfPlayersStatus() + { + int playerCount = Find.CurrentMap?.AsyncTime()?.CurrentPlayerCount ?? 0; + return new StatusBadge("P", playerCount > 0 ? Color.green : Color.red, $"{playerCount}", "Active players in the current map"); + } + + // Unified color logic for performance metrics + public static Color GetPerformanceColor(float value, float goodThreshold, float moderateThreshold, bool lowerIsBetter = false) + { + if (lowerIsBetter) + return value <= goodThreshold ? Color.green : value < moderateThreshold ? Color.yellow : Color.red; + + return value >= goodThreshold ? Color.green : value >= moderateThreshold ? Color.yellow : Color.red; + } + } +} diff --git a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs new file mode 100644 index 00000000..ae6ee9d6 --- /dev/null +++ b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs @@ -0,0 +1,479 @@ +using System; +using System.Linq; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Util; +using Multiplayer.Client.Patches; +using Multiplayer.Common; +using RimWorld; +using UnityEngine; +using Verse; +using System.Collections.Generic; // Added for List + +namespace Multiplayer.Client.DebugUi +{ + /// + /// Enhanced expandable debug panel that organizes existing comprehensive debug information + /// with modern UI/UX while preserving all developer functionality + /// + public static partial class SyncDebugPanel + { + // Panel state + private static bool isExpanded = false; + private static Vector2 scrollPosition = Vector2.zero; + + // Panel dimensions + private const float HeaderHeight = 40f; + private const float VisibleExpandedHeight = 400f; + private const float ContentHeight = 1300f; + private const float PanelWidth = 275f; + + // Visual constants + private const float Margin = 8f; + private const float StatusBadgePadding = 4f; + private const float SectionSpacing = 10f; + private const float LineHeight = 18f; + private const float LabelColumnWidth = 0.5f; + + /// + /// Main entry point for the enhanced debug panel + /// + public static float DoSyncDebugPanel(float y) + { + // Safety checks + if (Multiplayer.session == null || !MpVersion.IsDebug || !Multiplayer.ShowDevInfo || Multiplayer.WriterLog == null) return 0; + + try + { + float x = 100f; + float panelHeight = isExpanded ? VisibleExpandedHeight : HeaderHeight; + + Rect panelRect = new(x, y, PanelWidth, panelHeight); + + // Draw panel background + Widgets.DrawBoxSolid(panelRect, new Color(0f, 0f, 0f, 0.7f)); + Widgets.DrawBox(panelRect); + + if (isExpanded) + DrawExpandedPanel(panelRect); + else + DrawHeader(panelRect); + + return panelHeight + Margin; + } + catch (Exception ex) + { + // Fallback in case of any errors - don't crash the game + Log.Error($"SyncDebugPanel error: {ex.Message}"); + return 0; + } + } + + /// + /// Draw the compact status summary view + /// + private static void DrawHeader(Rect rect) + { + // Draw border around entire header for expanded mode only + if (isExpanded) + Widgets.DrawBox(rect); + + Rect contentRect = rect.ContractedBy(Margin); + + try + { + // Draw compact status indicators with text values + float currentX = contentRect.x + 2f; + float centerY = contentRect.y + (contentRect.height / 2f); + + // Draw all status badges + StatusBadge[] statusBadges = + [ + StatusBadge.GetSyncStatus(), + StatusBadge.GetPerformanceStatus(), + StatusBadge.GetVtrStatus(), + StatusBadge.GetNumOfPlayersStatus(), + StatusBadge.GetTickStatus() + ]; + + foreach (StatusBadge status in statusBadges) + { + currentX = DrawCompactStatusBadge(currentX, centerY, status); + currentX += StatusBadgePadding; + } + + // Button (expand/collapse) + float buttonWidth = 20f; + Rect buttonRect = new(contentRect.xMax - buttonWidth - StatusBadgePadding, contentRect.y + (contentRect.height - 16f) / 2f, buttonWidth, 16f); + + // Draw button as a badge (no border for consistency) + Widgets.DrawBoxSolid(buttonRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); + + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleCenter)) + { + if (Widgets.ButtonText(buttonRect, isExpanded ? "^" : "v")) + { + isExpanded = !isExpanded; + } + } + + // Click anywhere else to expand (only in compact mode) + if (!isExpanded && Event.current.type == EventType.MouseDown && + Event.current.button == 0 && + contentRect.Contains(Event.current.mousePosition) && + !buttonRect.Contains(Event.current.mousePosition)) + { + isExpanded = !isExpanded; + Event.current.Use(); + } + } + catch (Exception ex) + { + // Fallback display + using (MpStyle.Set(GameFont.Tiny).Set(Color.red).Set(TextAnchor.MiddleLeft)) + { + Widgets.Label(contentRect, $"Debug Panel Error: {ex.Message}"); + } + } + } + + /// + /// Draw a compact status badge with icon and text [🟢 SYNC] + /// + private static float DrawCompactStatusBadge(float x, float centerY, StatusBadge status) + { + // Calculate badge dimensions + string badgeText = $"{status.icon} {status.text}"; + float textWidth = Text.CalcSize(badgeText).x + 8f; // padding + float badgeHeight = 16f; + + Rect badgeRect = new(x, centerY - badgeHeight / 2f, textWidth, badgeHeight); + + // Draw badge background (no border for cleaner look) + Widgets.DrawBoxSolid(badgeRect, new Color(0.1f, 0.1f, 0.1f, 0.7f)); + + // Draw badge text + using (MpStyle.Set(GameFont.Tiny).Set(status.color).Set(TextAnchor.MiddleCenter)) + { + Widgets.Label(badgeRect, badgeText); + } + + // Add tooltip + if (!string.IsNullOrEmpty(status.tooltip)) + { + TooltipHandler.TipRegion(badgeRect, status.tooltip); + } + + return x + textWidth; + } + + /// + /// Draw the expanded comprehensive debug view + /// + private static void DrawExpandedPanel(Rect rect) + { + Rect headerRect = new(rect.x, rect.y, rect.width, 30f); + Rect contentRect = new(rect.x, rect.y + 30f, rect.width, rect.height - 30f); + + DrawHeader(headerRect); + Rect viewRect = new(0f, 0f, contentRect.width - 16f, ContentHeight); + Widgets.BeginScrollView(contentRect, ref scrollPosition, viewRect); + + float currentY = 0f; + + // Draw the content + currentY += DrawStatusSummarySection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawRngStatesSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawPerformanceSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawNetworkSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawCoreSystemSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawTimingSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawGameStateSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawRngDebugSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawCommandSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawMemoryPerformanceSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawMapManagementSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + + Widgets.EndScrollView(); + } + + /// + /// Generic section drawing method + /// + private static float DrawSection(float x, float y, float width, DebugSection section) + { + float startY = y; + + y = DrawSectionHeader(x, y, width, section.Title); + + foreach (DebugLine line in section.Lines) + { + y = DrawStatusLine(x, y, width, line.Label, line.Value, line.Color); + } + + return y - startY + SectionSpacing; + } + + /// + /// Draw status summary section with key indicators + /// + private static float DrawStatusSummarySection(float x, float y, float width) + { + StatusBadge syncStatus = StatusBadge.GetSyncStatus(); + StatusBadge performanceStatus = StatusBadge.GetPerformanceStatus(); + StatusBadge vtrStatus = StatusBadge.GetVtrStatus(); + StatusBadge tickStatus = StatusBadge.GetTickStatus(); + + DebugLine[] lines = [ + new("Sync Status:", syncStatus.text, syncStatus.color), + new("Performance:", performanceStatus.text, performanceStatus.color), + new("VTR Status:", vtrStatus.text, vtrStatus.color), + new("Tick Status:", tickStatus.text, tickStatus.color) + ]; + + DebugSection section = new("STATUS SUMMARY", lines); + return DrawSection(x, y, width, section); + } + + /// + /// Draw RNG states comparison section + /// + private static float DrawRngStatesSection(float x, float y, float width) + { + List lines; + + if (Find.CurrentMap?.AsyncTime() != null) + { + AsyncTimeComp async = Find.CurrentMap.AsyncTime(); + AsyncTime.AsyncWorldTimeComp worldAsync = Multiplayer.AsyncWorldTime; + + lines = [ + new("Current Map:", GetRngStatesString(async.randState), Color.white), + new("World RNG:", GetRngStatesString(worldAsync.randState), Color.white), + new("Round Mode:", $"{RoundMode.GetCurrentRoundMode()}", Color.white) + ]; + } + else + { + lines = [new("RNG States:", "No current map", Color.gray)]; + } + + return DrawSection(x, y, width, new("RNG STATES", lines.ToArray())); + } + + private static string GetRngStatesString(ulong randState) => $"{(uint)(randState >> 32):X8} | {(uint)randState:X8}"; + + /// + /// Draw performance metrics section + /// + private static float DrawPerformanceSection(float x, float y, float width) + { + // TPS + float tps = IngameUIPatch.tps; + Color tpsColor = StatusBadge.GetPerformanceColor(tps, 40f, 20f); + + // Frame time + float frameTime = Time.deltaTime * 1000f; + Color frameColor = StatusBadge.GetPerformanceColor(frameTime, 20f, 35f, true); + + DebugLine[] lines = [ + new("Map TPS:", $"{tps:F1}", tpsColor), + new("Frame Time:", $"{frameTime:F1}ms", frameColor), + new("Server TPT:", $"{TickPatch.serverTimePerTick:F1}ms", Color.white), + new("Avg Frame:", $"{TickPatch.avgFrameTime:F1}ms", Color.white) + ]; + + return DrawSection(x, y, width, new("PERFORMANCE", lines)); + } + + /// + /// Draw network and sync status section + /// + private static float DrawNetworkSyncSection(float x, float y, float width) + { + DebugLine[] lines; + + if (Multiplayer.session != null) + { + // Player count + int playerCount = Multiplayer.session.players.Count; + bool hasDesynced = Multiplayer.session.players.Any(p => p.status == PlayerStatus.Desynced); + Color playerColor = hasDesynced ? Color.red : Color.green; + + // Server status + string serverStatus = TickPatch.serverFrozen ? "Frozen" : "Running"; + Color serverColor = TickPatch.serverFrozen ? Color.yellow : Color.green; + + lines = [ + new("Players:", $"{playerCount}", playerColor), + new("Received Cmds:", $"{Multiplayer.session.receivedCmds}", Color.white), + new("Remote Sent:", $"{Multiplayer.session.remoteSentCmds}", Color.white), + new("Remote Tick:", $"{Multiplayer.session.remoteTickUntil}", Color.white), + new("Server:", serverStatus, serverColor), + ]; + } + else + { + lines = [(new("Network:", "No active session", Color.gray))]; + } + + return DrawSection(x, y, width, new("NETWORK & SYNC", lines)); + } + + /// + /// Draw core system data section + /// + private static float DrawCoreSystemSection(float x, float y, float width) + { + DebugLine[] coreLines = [ + new("Faction Stack:", $"{FactionContext.stack.Count}", Color.white), + new("Player Faction:", $"{Faction.OfPlayer.loadID}", Color.white), + new("Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID}", Color.white), + new("Next Thing ID:", $"{Find.UniqueIDsManager.nextThingID}", Color.white), + new("Next Job ID:", $"{Find.UniqueIDsManager.nextJobID}", Color.white), + new("Game Ticks:", $"{Find.TickManager.TicksGame}", Color.white), + new("Time Speed:", $"{Find.TickManager.CurTimeSpeed}", Color.white) + ]; + + return DrawSection(x, y, width, new("CORE SYSTEM", coreLines)); + } + + /// + /// Draw timing and sync data section + /// + private static float DrawTimingSyncSection(float x, float y, float width) + { + int timerLag = TickPatch.tickUntil - TickPatch.Timer; + Color lagColor = StatusBadge.GetPerformanceColor(timerLag, 15, 30); + + DebugLine[] timingLines = [ + new("Timer Lag:", $"{timerLag}", lagColor), + new("Timer:", $"{TickPatch.Timer}", Color.white), + new("Tick Until:", $"{TickPatch.tickUntil}", Color.white), + new("Raw Tick Timer:", $"{TickPatch.tickTimer.ElapsedMilliseconds}ms", Color.white), + new("World Settlements:", $"{Find.World.worldObjects.settlements.Count}", Color.white) + ]; + + return DrawSection(x, y, width, new("TIMING & SYNC", timingLines)); + } + + /// + /// Draw game state data section + /// + private static float DrawGameStateSection(float x, float y, float width) + { + AsyncTimeComp async = Find.CurrentMap.AsyncTime(); + + DebugLine[] gameStateLines = [ + new("Classic Mode:", $"{Find.IdeoManager.classicMode}", Color.white), + new("Client Opinions:", $"{Multiplayer.game.sync.knownClientOpinions.Count}", Color.white), + new("First Opinion Tick:", $"{Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick}", Color.white), + new("Map Ticks:", $"{async.mapTicks}", Color.white), + new("Frozen At:", $"{TickPatch.frozenAt}", Color.white) + ]; + + return DrawSection(x, y, width, new("GAME STATE", gameStateLines)); + } + + /// + /// Draw RNG and debug data section + /// + private static float DrawRngDebugSection(float x, float y, float width) + { + DebugLine[] rngLines = [ + new("Rand Calls:", $"{DeferredStackTracing.acc}", Color.white), + new("Max Trace Depth:", $"{DeferredStackTracing.maxTraceDepth}", Color.white), + new("Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white), + new("Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white) + ]; + + return DrawSection(x, y, width, new("RNG & DEBUG", rngLines)); + } + + /// + /// Draw command and sync data section + /// + private static float DrawCommandSyncSection(float x, float y, float width) + { + AsyncTimeComp async = Find.CurrentMap.AsyncTime(); + + DebugLine[] commandLines = [ + new("Async Commands:", $"{async.cmds.Count}", Color.white), + new("World Commands:", $"{Multiplayer.AsyncWorldTime.cmds.Count}", Color.white), + new("Force Normal Speed:", $"{async.slower.forceNormalSpeedUntil}", Color.white), + new("Async Time Status:", $"{Multiplayer.GameComp.asyncTime}", Color.white), + new("Buffered Changes:", $"{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}", Color.white) + ]; + + return DrawSection(x, y, width, new("COMMAND & SYNC", commandLines)); + } + + /// + /// Draw memory and performance data section + /// + private static float DrawMemoryPerformanceSection(float x, float y, float width) + { + float serverTpt = TickPatch.tickUntil - TickPatch.Timer <= 3 ? TickPatch.serverTimePerTick * 1.2f : + TickPatch.tickUntil - TickPatch.Timer >= 7 ? TickPatch.serverTimePerTick * 0.8f : + TickPatch.serverTimePerTick; + + DebugLine[] memoryLines = [ + new("World Pawns:", $"{Find.WorldPawns.AllPawnsAliveOrDead.Count}", Color.white), + new("Pool Free Items:", $"{SimplePool.FreeItemsCount}", Color.white), + new("Calc Server TPT:", $"{serverTpt:F1}ms", Color.white) + ]; + + return DrawSection(x, y, width, new("MEMORY & PERFORMANCE", memoryLines)); + } + + /// + /// Draw map management data section + /// + private static float DrawMapManagementSection(float x, float y, float width) + { + DebugLine[] mapLines = [ + new("Haul Destinations:", $"{Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count}", Color.white), + new("Designations:", $"{Find.CurrentMap.designationManager.designationsByDef.Count}", Color.white), + new("Haulable Items:", $"{Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white), + new("Mining Designations:", $"{Find.CurrentMap.designationManager.SpawnedDesignationsOfDef(DesignationDefOf.Mine).Count()}", Color.white), + new("First Ideology ID:", $"{Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}", Color.white) + ]; + + return DrawSection(x, y, width, new("MAP MANAGEMENT", mapLines)); + } + + // Helper methods for UI drawing + + private static float DrawSectionHeader(float x, float y, float width, string title) + { + Rect headerRect = new(x, y, width, LineHeight + 2f); + using (MpStyle.Set(GameFont.Small).Set(Color.cyan).Set(TextAnchor.MiddleLeft)) + { + Widgets.Label(headerRect, $"── {title} ──"); + } + return y + LineHeight + 6f; // More spacing after headers + } + + private static float DrawStatusLine(float x, float y, float width, string label, string value, Color valueColor) + { + // Use consistent column widths and add padding + float labelWidth = width * LabelColumnWidth; + float valueWidth = width * (1f - LabelColumnWidth); + + // Label with right padding + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleLeft)) + { + Rect labelRect = new(x, y, labelWidth - 4f, LineHeight); + Widgets.Label(labelRect, label); + } + + // Value with left padding + using (MpStyle.Set(GameFont.Tiny).Set(valueColor).Set(TextAnchor.MiddleLeft)) + { + Rect valueRect = new(x + labelWidth + 4f, y, valueWidth - 4f, LineHeight); + Widgets.Label(valueRect, value); + } + + return y + LineHeight + 1f; // Small padding between lines + } + } +} diff --git a/Source/Client/UI/SyncDebugPanel.cs b/Source/Client/UI/SyncDebugPanel.cs deleted file mode 100644 index a74343ec..00000000 --- a/Source/Client/UI/SyncDebugPanel.cs +++ /dev/null @@ -1,725 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Util; -using Multiplayer.Common; -using RimWorld; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client.DebugUi -{ - /// - /// Enhanced expandable debug panel that organizes existing comprehensive debug information - /// with modern UI/UX while preserving all developer functionality - /// - public static class SyncDebugPanel - { - // Panel state - private static bool isExpanded = false; - private static Vector2 scrollPosition = Vector2.zero; - - // Panel dimensions - private const float CompactHeight = 40f; - private const float ExpandedMaxHeight = 400f; - private const float PanelWidth = 350f; - private const float Margin = 8f; - - // Visual constants - private const float SectionSpacing = 15f; - private const float LineHeight = 18f; - private const float LabelColumnWidth = 0.45f; // 45% for labels, 55% for values - - /// - /// Main entry point for the enhanced debug panel - /// - public static float DoSyncDebugPanel(float y) - { - // Safety checks - if (Multiplayer.session == null || !MpVersion.IsDebug || !Multiplayer.ShowDevInfo || Multiplayer.WriterLog == null) return 0; - - try - { - float x = Margin; - float panelHeight = isExpanded ? CalculateExpandedHeight() : CompactHeight; - - Rect panelRect = new Rect(x, y, PanelWidth, panelHeight); - - // Draw panel background - Widgets.DrawBoxSolid(panelRect, new Color(0f, 0f, 0f, 0.7f)); - Widgets.DrawBox(panelRect); - - if (isExpanded) - { - DrawExpandedPanel(panelRect); - } - else - { - DrawCompactPanel(panelRect); - } - - return panelHeight + Margin; - } - catch (Exception ex) - { - // Fallback in case of any errors - don't crash the game - Log.Error($"SyncDebugPanel error: {ex.Message}"); - return 0; - } - } - - /// - /// Draw the compact status summary view - /// - private static void DrawCompactPanel(Rect rect) - { - Rect contentRect = rect.ContractedBy(Margin); - - try - { - // Get status information - var syncStatus = GetSyncStatus(); - var performanceStatus = GetPerformanceStatus(); - var errorStatus = GetErrorStatus(); - var tickStatus = GetTickStatus(); - - // Draw compact status indicators with text values - float currentX = contentRect.x + 2f; - float centerY = contentRect.y + (contentRect.height / 2f); - - // Sync status [🟢 SYNC] - currentX = DrawCompactStatusBadge(currentX, centerY, syncStatus.icon, syncStatus.text, syncStatus.color, syncStatus.tooltip); - currentX += 4f; // spacing between badges - - // Performance status [⚡ 45.2] - currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); - currentX += 4f; - - // RNG status [📊 0] - currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); - currentX += 4f; - - // Tick status [🎯 3] - currentX = DrawCompactStatusBadge(currentX, centerY, tickStatus.icon, tickStatus.text, tickStatus.color, tickStatus.tooltip); - - // Expand button [v] - float buttonWidth = 20f; - Rect expandRect = new Rect(contentRect.xMax - buttonWidth - 2f, - contentRect.y + (contentRect.height - 16f) / 2f, - buttonWidth, 16f); - - // Draw expand button as a badge (no border for consistency) - Widgets.DrawBoxSolid(expandRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); - - using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleCenter)) - { - if (Widgets.ButtonText(expandRect, "v")) - { - isExpanded = true; - } - } - - // Click anywhere else to expand - if (Event.current.type == EventType.MouseDown && - Event.current.button == 0 && - contentRect.Contains(Event.current.mousePosition) && - !expandRect.Contains(Event.current.mousePosition)) - { - isExpanded = true; - Event.current.Use(); - } - } - catch (Exception ex) - { - // Fallback display for compact mode - using (MpStyle.Set(GameFont.Tiny).Set(Color.red).Set(TextAnchor.MiddleLeft)) - { - Widgets.Label(contentRect, $"Debug Panel Error: {ex.Message}"); - } - } - } - - /// - /// Draw a compact status badge with icon and text [🟢 SYNC] - /// - private static float DrawCompactStatusBadge(float x, float centerY, string icon, string text, Color color, string tooltip) - { - // Calculate badge dimensions - string badgeText = $"{icon} {text}"; - float textWidth = Text.CalcSize(badgeText).x + 8f; // padding - float badgeHeight = 16f; - - Rect badgeRect = new Rect(x, centerY - badgeHeight / 2f, textWidth, badgeHeight); - - // Draw badge background (no border for cleaner look) - Widgets.DrawBoxSolid(badgeRect, new Color(0.1f, 0.1f, 0.1f, 0.7f)); - - // Draw badge text - using (MpStyle.Set(GameFont.Tiny).Set(color).Set(TextAnchor.MiddleCenter)) - { - Widgets.Label(badgeRect, badgeText); - } - - // Add tooltip - if (!string.IsNullOrEmpty(tooltip)) - { - TooltipHandler.TipRegion(badgeRect, tooltip); - } - - return x + textWidth; - } - - /// - /// Draw the expanded comprehensive debug view - /// - private static void DrawExpandedPanel(Rect rect) - { - Rect headerRect = new Rect(rect.x, rect.y, rect.width, 30f); - Rect contentRect = new Rect(rect.x, rect.y + 30f, rect.width, rect.height - 30f); - - // Draw header - DrawPanelHeader(headerRect); - - // Draw scrollable content - Rect viewRect = new Rect(0f, 0f, contentRect.width - 16f, GetContentHeight()); - Widgets.BeginScrollView(contentRect, ref scrollPosition, viewRect); - - float currentY = 0f; - - // Status Summary Section - currentY += DrawStatusSummarySection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); - currentY += SectionSpacing; - - // RNG States Section - currentY += DrawRNGStatesSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); - currentY += SectionSpacing; - - // Performance Section - currentY += DrawPerformanceSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); - currentY += SectionSpacing; - - // Network & Sync Section - currentY += DrawNetworkSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); - currentY += SectionSpacing; - - // Detailed Debug Section (existing comprehensive information) - currentY += DrawDetailedDebugSection(viewRect.x + Margin, currentY, viewRect.width - Margin * 2); - - Widgets.EndScrollView(); - } - - /// - /// Draw the panel header with title and controls - /// - private static void DrawPanelHeader(Rect rect) - { - // Draw border around entire header for expanded mode - Widgets.DrawBox(rect); - - try - { - // Get status information - same as compact mode - var syncStatus = GetSyncStatus(); - var performanceStatus = GetPerformanceStatus(); - var errorStatus = GetErrorStatus(); - var tickStatus = GetTickStatus(); - - // Use SAME content area as compact mode (contract by margin) - Rect contentRect = rect.ContractedBy(Margin); - - // Draw status badges - EXACTLY same layout as compact mode - float currentX = contentRect.x + 2f; // Same margin as compact - float centerY = contentRect.y + (contentRect.height / 2f); - - // Sync status [● SYNC] - currentX = DrawCompactStatusBadge(currentX, centerY, syncStatus.icon, syncStatus.text, syncStatus.color, syncStatus.tooltip); - currentX += 4f; // Same spacing as compact - - // Performance status [▲ 45.2] - currentX = DrawCompactStatusBadge(currentX, centerY, performanceStatus.icon, performanceStatus.text, performanceStatus.color, performanceStatus.tooltip); - currentX += 4f; - - // RNG status [■ 0] - currentX = DrawCompactStatusBadge(currentX, centerY, errorStatus.icon, errorStatus.text, errorStatus.color, errorStatus.tooltip); - currentX += 4f; - - // Tick status [♦ 3] - currentX = DrawCompactStatusBadge(currentX, centerY, tickStatus.icon, tickStatus.text, tickStatus.color, tickStatus.tooltip); - - // Collapse button [^] - same style as expand button but no extra border - float buttonWidth = 20f; - Rect collapseRect = new Rect(contentRect.xMax - buttonWidth - 2f, // Same positioning as compact - contentRect.y + (contentRect.height - 16f) / 2f, - buttonWidth, 16f); - - // Draw collapse button with same styling as compact expand button (no border) - Widgets.DrawBoxSolid(collapseRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); - - using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleCenter)) - { - if (Widgets.ButtonText(collapseRect, "^")) - { - isExpanded = false; - } - } - } - catch (Exception ex) - { - // Fallback to simple header - using (MpStyle.Set(GameFont.Small).Set(Color.white).Set(TextAnchor.MiddleLeft)) - { - Rect titleRect = new Rect(rect.x + Margin, rect.y, rect.width - 100f, rect.height); - Widgets.Label(titleRect, "MULTIPLAYER DEBUG"); - } - - Rect collapseRect = new Rect(rect.xMax - 80f, rect.y + 5f, 70f, 20f); - if (Widgets.ButtonText(collapseRect, "Collapse")) - { - isExpanded = false; - } - } - } - - /// - /// Draw status summary section with key indicators - /// - private static float DrawStatusSummarySection(float x, float y, float width) - { - float startY = y; - - // Section header - y = DrawSectionHeader(x, y, width, "STATUS SUMMARY"); - - var syncStatus = GetSyncStatus(); - var performanceStatus = GetPerformanceStatus(); - var errorStatus = GetErrorStatus(); - var tickStatus = GetTickStatus(); - - // Status lines - y = DrawStatusLine(x, y, width, "Sync Status:", syncStatus.text, syncStatus.color); - y = DrawStatusLine(x, y, width, "Performance:", performanceStatus.text, performanceStatus.color); - y = DrawStatusLine(x, y, width, "Status:", errorStatus.text, errorStatus.color); - y = DrawStatusLine(x, y, width, "Tick Status:", tickStatus.text, tickStatus.color); - - return y - startY; - } - - /// - /// Draw RNG states comparison section - /// - private static float DrawRNGStatesSection(float x, float y, float width) - { - float startY = y; - - y = DrawSectionHeader(x, y, width, "RNG STATES"); - - if (Find.CurrentMap?.AsyncTime() != null) - { - var async = Find.CurrentMap.AsyncTime(); - var worldAsync = Multiplayer.AsyncWorldTime; - - // Current Map RNG state - string mapRngLow = $"{(uint)async.randState:X8}"; - string mapRngHigh = $"{(uint)(async.randState >> 32):X8}"; - y = DrawStatusLine(x, y, width, "Current Map:", $"{mapRngHigh} | {mapRngLow}", Color.white); - - // World RNG state - string worldRngLow = $"{(uint)worldAsync.randState:X8}"; - string worldRngHigh = $"{(uint)(worldAsync.randState >> 32):X8}"; - y = DrawStatusLine(x, y, width, "World RNG:", $"{worldRngHigh} | {worldRngLow}", Color.white); - - // Round Mode - var roundMode = RoundMode.GetCurrentRoundMode(); - y = DrawStatusLine(x, y, width, "Round Mode:", $"{roundMode}", Color.white); - } - else - { - y = DrawStatusLine(x, y, width, "RNG States:", "No current map", Color.gray); - } - - return y - startY; - } - - /// - /// Draw performance metrics section - /// - private static float DrawPerformanceSection(float x, float y, float width) - { - float startY = y; - - y = DrawSectionHeader(x, y, width, "PERFORMANCE"); - - // TPS - float tps = IngameUIPatch.tps; - Color tpsColor = tps > 40f ? Color.green : tps > 20f ? Color.yellow : Color.red; - y = DrawStatusLine(x, y, width, "Map TPS:", $"{tps:F1}", tpsColor); - - // Frame time - float frameTime = Time.deltaTime * 1000f; - Color frameColor = frameTime < 20f ? Color.green : frameTime < 35f ? Color.yellow : Color.red; - y = DrawStatusLine(x, y, width, "Frame Time:", $"{frameTime:F1}ms", frameColor); - - // Server time per tick - float serverTpt = TickPatch.serverTimePerTick; - y = DrawStatusLine(x, y, width, "Server TPT:", $"{serverTpt:F1}ms", Color.white); - - // Average frame time - y = DrawStatusLine(x, y, width, "Avg Frame:", $"{TickPatch.avgFrameTime:F1}ms", Color.white); - - return y - startY; - } - - /// - /// Draw network and sync status section - /// - private static float DrawNetworkSyncSection(float x, float y, float width) - { - float startY = y; - - y = DrawSectionHeader(x, y, width, "NETWORK & SYNC"); - - if (Multiplayer.session != null) - { - // Player count - int playerCount = Multiplayer.session.players.Count; - bool hasDesynced = Multiplayer.session.players.Any(p => p.status == PlayerStatus.Desynced); - Color playerColor = hasDesynced ? Color.red : Color.green; - y = DrawStatusLine(x, y, width, "Players:", $"{playerCount}", playerColor); - - // Commands - y = DrawStatusLine(x, y, width, "Received Cmds:", $"{Multiplayer.session.receivedCmds}", Color.white); - y = DrawStatusLine(x, y, width, "Remote Sent:", $"{Multiplayer.session.remoteSentCmds}", Color.white); - y = DrawStatusLine(x, y, width, "Remote Tick:", $"{Multiplayer.session.remoteTickUntil}", Color.white); - - // Server status - string serverStatus = TickPatch.serverFrozen ? "Frozen" : "Running"; - Color serverColor = TickPatch.serverFrozen ? Color.yellow : Color.green; - y = DrawStatusLine(x, y, width, "Server:", serverStatus, serverColor); - } - else - { - y = DrawStatusLine(x, y, width, "Network:", "No active session", Color.gray); - } - - return y - startY; - } - - /// - /// Draw detailed debug section with existing comprehensive information - /// - private static float DrawDetailedDebugSection(float x, float y, float width) - { - float startY = y; - - if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) - { - var async = Find.CurrentMap.AsyncTime(); - - // ===== CORE SYSTEM DATA ===== - y = DrawSectionHeader(x, y, width, "CORE SYSTEM"); - y = DrawStatusLine(x, y, width, "Faction Stack:", $"{FactionContext.stack.Count}", Color.white); - y = DrawStatusLine(x, y, width, "Player Faction:", $"{Faction.OfPlayer.loadID}", Color.white); - y = DrawStatusLine(x, y, width, "Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID}", Color.white); - y = DrawStatusLine(x, y, width, "Next Thing ID:", $"{Find.UniqueIDsManager.nextThingID}", Color.white); - y = DrawStatusLine(x, y, width, "Next Job ID:", $"{Find.UniqueIDsManager.nextJobID}", Color.white); - y = DrawStatusLine(x, y, width, "Game Ticks:", $"{Find.TickManager.TicksGame}", Color.white); - y = DrawStatusLine(x, y, width, "Time Speed:", $"{Find.TickManager.CurTimeSpeed}", Color.white); - y += SectionSpacing; - - // ===== TIMING & SYNC DATA ===== - y = DrawSectionHeader(x, y, width, "TIMING & SYNC"); - int timerLag = TickPatch.tickUntil - TickPatch.Timer; - Color lagColor = timerLag > 30 ? Color.red : timerLag > 15 ? Color.yellow : Color.green; - y = DrawStatusLine(x, y, width, "Timer Lag:", $"{timerLag}", lagColor); - y = DrawStatusLine(x, y, width, "Timer:", $"{TickPatch.Timer}", Color.white); - y = DrawStatusLine(x, y, width, "Tick Until:", $"{TickPatch.tickUntil}", Color.white); - y = DrawStatusLine(x, y, width, "Raw Tick Timer:", $"{TickPatch.tickTimer.ElapsedMilliseconds}ms", Color.white); - y = DrawStatusLine(x, y, width, "World Settlements:", $"{Find.World.worldObjects.settlements.Count}", Color.white); - y += SectionSpacing; - - // ===== GAME STATE DATA ===== - y = DrawSectionHeader(x, y, width, "GAME STATE"); - y = DrawStatusLine(x, y, width, "Classic Mode:", $"{Find.IdeoManager.classicMode}", Color.white); - y = DrawStatusLine(x, y, width, "Client Opinions:", $"{Multiplayer.game.sync.knownClientOpinions.Count}", Color.white); - y = DrawStatusLine(x, y, width, "First Opinion Tick:", $"{Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick}", Color.white); - y = DrawStatusLine(x, y, width, "Map Ticks:", $"{async.mapTicks}", Color.white); - y = DrawStatusLine(x, y, width, "Frozen At:", $"{TickPatch.frozenAt}", Color.white); - y += SectionSpacing; - - // ===== RNG & DEBUG DATA ===== - y = DrawSectionHeader(x, y, width, "RNG & DEBUG"); - y = DrawStatusLine(x, y, width, "Rand Calls:", $"{DeferredStackTracing.acc}", Color.white); - y = DrawStatusLine(x, y, width, "Max Trace Depth:", $"{DeferredStackTracing.maxTraceDepth}", Color.white); - y = DrawStatusLine(x, y, width, "Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white); - y = DrawStatusLine(x, y, width, "Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white); - y += SectionSpacing; - - // ===== COMMAND & SYNC DATA ===== - y = DrawSectionHeader(x, y, width, "COMMAND & SYNC"); - y = DrawStatusLine(x, y, width, "Async Commands:", $"{async.cmds.Count}", Color.white); - y = DrawStatusLine(x, y, width, "World Commands:", $"{Multiplayer.AsyncWorldTime.cmds.Count}", Color.white); - y = DrawStatusLine(x, y, width, "Force Normal Speed:", $"{async.slower.forceNormalSpeedUntil}", Color.white); - y = DrawStatusLine(x, y, width, "Async Time Status:", $"{Multiplayer.GameComp.asyncTime}", Color.white); - y = DrawStatusLine(x, y, width, "Buffered Changes:", $"{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}", Color.white); - y += SectionSpacing; - - // ===== MEMORY & PERFORMANCE DATA ===== - y = DrawSectionHeader(x, y, width, "MEMORY & PERFORMANCE"); - y = DrawStatusLine(x, y, width, "World Pawns:", $"{Find.WorldPawns.AllPawnsAliveOrDead.Count}", Color.white); - y = DrawStatusLine(x, y, width, "Pool Free Items:", $"{SimplePool.FreeItemsCount}", Color.white); - - // Calculated server performance - var calcStpt = TickPatch.tickUntil - TickPatch.Timer <= 3 ? TickPatch.serverTimePerTick * 1.2f : - TickPatch.tickUntil - TickPatch.Timer >= 7 ? TickPatch.serverTimePerTick * 0.8f : - TickPatch.serverTimePerTick; - y = DrawStatusLine(x, y, width, "Calc Server TPT:", $"{calcStpt:F1}ms", Color.white); - y += SectionSpacing; - - // ===== MAP MANAGEMENT DATA ===== - y = DrawSectionHeader(x, y, width, "MAP MANAGEMENT"); - y = DrawStatusLine(x, y, width, "Haul Destinations:", $"{Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count}", Color.white); - y = DrawStatusLine(x, y, width, "Designations:", $"{Find.CurrentMap.designationManager.designationsByDef.Count}", Color.white); - y = DrawStatusLine(x, y, width, "Haulable Items:", $"{Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white); - y = DrawStatusLine(x, y, width, "Mining Designations:", $"{Find.CurrentMap.designationManager.SpawnedDesignationsOfDef(DesignationDefOf.Mine).Count()}", Color.white); - y = DrawStatusLine(x, y, width, "First Ideology ID:", $"{Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}", Color.white); - - // Faction-specific data (if available) - if (Find.CurrentMap.ParentFaction != null) - { - int faction = Find.CurrentMap.ParentFaction.loadID; - MultiplayerMapComp comp = Find.CurrentMap.MpComp(); - FactionMapData data = comp.factionData.TryGetValue(faction); - - if (data != null) - { - y = DrawStatusLine(x, y, width, "Faction Haulables:", $"{data.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white); - y = DrawStatusLine(x, y, width, "Faction Haul Groups:", $"{data.haulDestinationManager.AllGroupsListForReading.Count}", Color.white); - } - } - } - - return y - startY; - } - - // Helper methods for UI drawing - - private static void DrawStatusIcon(Rect rect, string icon, Color color, string tooltip) - { - try - { - // Draw background for debugging - Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.5f)); - - using (MpStyle.Set(color).Set(TextAnchor.MiddleCenter).Set(GameFont.Small)) - { - Widgets.Label(rect, icon); - } - - if (!string.IsNullOrEmpty(tooltip)) - { - TooltipHandler.TipRegion(rect, tooltip); - } - } - catch (Exception ex) - { - // Fallback: draw a simple colored rectangle - Widgets.DrawBoxSolid(rect, color); - Log.Warning($"DrawStatusIcon error: {ex.Message}"); - } - } - - private static float DrawSectionHeader(float x, float y, float width, string title) - { - Rect headerRect = new Rect(x, y, width, LineHeight + 2f); - using (MpStyle.Set(GameFont.Small).Set(Color.cyan).Set(TextAnchor.MiddleLeft)) - { - Widgets.Label(headerRect, $"── {title} ──"); - } - return y + LineHeight + 6f; // More spacing after headers - } - - private static float DrawStatusLine(float x, float y, float width, string label, string value, Color valueColor) - { - // Use consistent column widths and add padding - float labelWidth = width * LabelColumnWidth; - float valueWidth = width * (1f - LabelColumnWidth); - - // Label with right padding - using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleLeft)) - { - Rect labelRect = new Rect(x, y, labelWidth - 4f, LineHeight); - Widgets.Label(labelRect, label); - } - - // Value with left padding - using (MpStyle.Set(GameFont.Tiny).Set(valueColor).Set(TextAnchor.MiddleLeft)) - { - Rect valueRect = new Rect(x + labelWidth + 4f, y, valueWidth - 4f, LineHeight); - Widgets.Label(valueRect, value); - } - - return y + LineHeight + 1f; // Small padding between lines - } - - // Status calculation methods - - private static (string icon, Color color, string text, string tooltip) GetSyncStatus() - { - try - { - // Check if there's an active multiplayer session and desync detection - if (Multiplayer.session != null) - { - bool hasDesynced = Multiplayer.session.players.Any(p => p.status == PlayerStatus.Desynced); - - if (hasDesynced) - return ("●", Color.red, "DESYNC", "Players have desynced!"); - - // Check if we're in a valid sync state - if (Multiplayer.session.desynced) - return ("●", Color.red, "DESYNC", "Session has desynced"); - - return ("●", Color.green, "SYNC", "All players are synchronized"); - } - - return ("●", Color.yellow, "N/A", "No active session"); - } - catch (Exception ex) - { - Log.Warning($"GetSyncStatus error: {ex.Message}"); - return ("?", Color.gray, "ERR", "Error getting sync status"); - } - } - - private static (string icon, Color color, string text, string tooltip) GetPerformanceStatus() - { - try - { - float tps = IngameUIPatch.tps; - - if (tps > 40f) - return ("▲", Color.green, $"{tps:F1}", "Performance is good"); - else if (tps > 20f) - return ("▲", Color.yellow, $"{tps:F1}", "Performance is moderate"); - else - return ("▲", Color.red, $"{tps:F1}", "Performance is poor"); - } - catch (Exception ex) - { - Log.Warning($"GetPerformanceStatus error: {ex.Message}"); - return ("?", Color.gray, "ERR", "Error getting performance status"); - } - } - - private static (string icon, Color color, string text, string tooltip) GetErrorStatus() - { - try - { - // For now, return a placeholder - we can add a more useful metric here later - return ("■", Color.gray, "N/A", "No critical metric"); - } - catch (Exception ex) - { - Log.Warning($"GetErrorStatus error: {ex.Message}"); - return ("?", Color.gray, "ERR", "Error getting status"); - } - } - - private static (string icon, Color color, string text, string tooltip) GetTickStatus() - { - try - { - int behind = TickPatch.tickUntil - TickPatch.Timer; - - if (behind <= 5) - return ("♦", Color.green, $"{behind}", "Timing is good"); - else if (behind <= 15) - return ("♦", Color.yellow, $"{behind}", "Slightly behind"); - else - return ("♦", Color.red, $"{behind}", "Significantly behind"); - } - catch (Exception ex) - { - Log.Warning($"GetTickStatus error: {ex.Message}"); - return ("?", Color.gray, "ERR", "Error getting tick status"); - } - } - - // Utility methods - - private static float CalculateExpandedHeight() - { - return Math.Min(ExpandedMaxHeight, GetContentHeight() + 50f); - } - - private static float GetContentHeight() - { - // Calculate actual content height dynamically by counting lines - float height = 0f; - - // Status summary section - height += LineHeight + 6f; // Header - height += 4 * (LineHeight + 1f); // 4 status lines - height += SectionSpacing; - - // RNG states section - height += LineHeight + 6f; // Header - if (Find.CurrentMap?.AsyncTime() != null) - { - // Base RNG lines: Current Map, World, Round Mode - height += 3 * (LineHeight + 1f); - } - else - { - height += 1 * (LineHeight + 1f); // 1 "No current map" line - } - height += SectionSpacing; - - // Performance section - height += LineHeight + 6f; // Header - height += 4 * (LineHeight + 1f); // 4 performance lines - height += SectionSpacing; - - // Network & sync section - height += LineHeight + 6f; // Header - if (Multiplayer.session != null) - { - height += 5 * (LineHeight + 1f); // 5 network lines - } - else - { - height += 1 * (LineHeight + 1f); // 1 "No active session" line - } - height += SectionSpacing; - - // Detailed debug section (organized into 6 subsections) - if (Multiplayer.ShowDevInfo && Find.CurrentMap != null) - { - // Core System: 7 lines + header - height += LineHeight + 6f + 7 * (LineHeight + 1f) + SectionSpacing; - // Timing & Sync: 5 lines + header - height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; - // Game State: 5 lines + header - height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; - // Map Management: 5-7 lines + header (including faction data) - moved to bottom - height += LineHeight + 6f + 7 * (LineHeight + 1f); - // RNG & Debug: 4 lines + header - height += LineHeight + 6f + 4 * (LineHeight + 1f) + SectionSpacing; - // Command & Sync: 5 lines + header - height += LineHeight + 6f + 5 * (LineHeight + 1f) + SectionSpacing; - // Memory & Performance: 3 lines + header - height += LineHeight + 6f + 3 * (LineHeight + 1f); - } - else - { - height += 1 * (LineHeight + 1f); // Minimal fallback - } - - // Small bottom padding to prevent cutoff - height += 20f; - - return height; - } - } -} From 1a15ecb4953b58e266baa66ca49159047b42801b Mon Sep 17 00:00:00 2001 From: Reznal Date: Thu, 10 Jul 2025 23:32:18 +1000 Subject: [PATCH 22/38] API Update Renamed WorldRenderedNow To WorldRendered --- .gitignore | 6 ++++-- Source/Client/Multiplayer.csproj | 2 +- Source/Common/Common.csproj | 2 +- Source/NuGet.config | 7 +++++++ .../RimWorld.MultiplayerAPI.0.6.0.nupkg | Bin 0 -> 12544 bytes 5 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 Source/NuGet.config create mode 100644 Source/Packages/RimWorld.MultiplayerAPI.0.6.0.nupkg diff --git a/.gitignore b/.gitignore index f93c7ab3..799f6bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -151,10 +151,12 @@ publish/ # in these scripts will be unencrypted PublishScripts/ +# Commented out .nupkg and **/packages/* so we can use a custom package # NuGet Packages -*.nupkg +# *.nupkg # The packages folder can be ignored because of Package Restore -**/packages/* +# **/packages/* + # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed diff --git a/Source/Client/Multiplayer.csproj b/Source/Client/Multiplayer.csproj index d0db8fa4..e2be4e8b 100644 --- a/Source/Client/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -34,7 +34,7 @@ - + diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index e002ddab..787a1dfe 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -16,7 +16,7 @@ - + diff --git a/Source/NuGet.config b/Source/NuGet.config new file mode 100644 index 00000000..362f7cd1 --- /dev/null +++ b/Source/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Source/Packages/RimWorld.MultiplayerAPI.0.6.0.nupkg b/Source/Packages/RimWorld.MultiplayerAPI.0.6.0.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..f0af65f9ee7411f4c22903236704aaa57a3dd891 GIT binary patch literal 12544 zcmcJ0Wl$Z#wk}SP&Bh_f#@$_ly9IX*Y~0;7xJz)~xDyEO1a}DTEPO)h#u7=IN&JowTkj};=8(Of<{5&^soSnXv61k zTvX~Q!|feon0T%X>(z;Pm9gnH6&2acvjvRZgchgU6OVZB_Ii7Rg;M?(i#jWBV@teS zlLa0M3iOUpwX)N4aJDsN{_JM!YUOBa>}BRGsw~TF@8;rY25wLvvtMOJ??4(CH(wcFVXP`@*`{8GQry(esLK`)cPb8}Pema?AD?$4U<&%Y?da}<`pBngm$?k^R>9$G9=TJ)E%@@{Lg=ov2!v#s1*Kvc65O#6|oUYl4S z?n1|rX011yL(sr3rKY1}8+KS|!}nc|HUi{#5c+kqFg=wXV?qQ7HIeeNjF50e505!6 z-@Hzn2_4_+{6xFt4xjO>duP7;&*2#N_ra5KH|&UAVGpQcC?R9m`3D9eI#SHQMC|YE zfmuYmnBnR6wMK#i?uh!fHucx;1cs|+7jxSH@L4L1e3mVRP4Rk@YS!}NBh3=ir>=}z zW&Q1@&*UM}=CD)MaB!~|LR|G-$PrxndxrZVHj0Q6>z?}yf-uU2xL(fto-*KNWr~v_ zmETLAuUZ;kkpCq6ewcQD5q2$4yxlDy`j&;`i%fJM^@JpHT8|*&r&WnLeT5aI-3fKz z`O}F~AT$Is9c#AYxH3o^EZc=KEhUaEYuN-9Rz)W=ZYZx0K(DMXfnlS8k>So_t$;aJ zDos_61B9L&RB%)tgj8X-)=;qnB3`fofm^aah+OOxh!G=7#>zUl@TLg_y6AcWXnS}N zAH1Ob{R4%$dRR#Pg|2_`Gv#Z`{KeFQlxZyd4#C6u?l!3HX`a7Fz9XEOht^N6%@LTc zA#F;Xs57U;p?#p@HH3sbEB`Wi|n>qpwS%@ zS|M28x&$qaKT+$d(ISBXghlnG8ToA%AOn5j&r_j+Be&O)wTbN`fhXV+sq zdYvml6&RdHwIFSUAMrB{x*|B%cdJ4yTyiVgVD+rbg5=nVxsvls{9GMCca<=41YHw) zD0@_uaIo{G0>WKOzDZ(j`t&qq%F~a)r3VkmKI?MMl##)Rtb)3*O8^$ZMzwI3Pglrj zHf+D@kHuPHSN|8)Bxe>nL~xeCB|EMf9^JJ`76GoDSvt(Fca^Q?^zZ5uRV!TxC$ozI zgTl#bSr!R0uhtrU>Nj7pp7x!E8AD6+*zsD@S9TB1BE3ms>cxdvpLV>o#tOo)Mo!Ad zl%kcC3n&ve=c!haQ-d=B(o`c!jaF;r)wDk#D4a({5KF5CS9PUL+1eTs2g}J$l1=#@ ze^P9cl#kX4wJb34)}FiNu~k%Y$pv}ebvusB_!LkTw)LpG%n=+jcv0@c!XYbQq)Rxw z&9eU2y*3)TbCV<5b~2^?_X>W>zF4CeC)bLgibN`kJO$FdO;kIeOlD`@38%+&O3oU< zjuaYg9hzPJN~(8P&NZ&yR^+y1m$|xFa(E2&TQOdH{(1jR|5E6Y1ZP@^^lfC3pQW~= ze42RJlYufHr$j7383K<@0;K=h^)rWp3TDO0g_1en>8jDkMWUFUQ=0dryt`%st(^0- zGQqc+(4Wa_>+%L3&~*A)S1B+nwCibp%f4p!Qo6a^7`#Sx+-g^ON<2HyaAC(Rj$;NN z`lXuT{01s~-63m!7)lM*qQQ^O_uN;-?a{yL&t1}x97?60yNFRTGqV)!gxoeVD3Qoe zNI6i_{XhO$W86xj|Ki;Y&T4%XxOl{c4WJ49ebTPd*uRdaWK`Ex2d^xC+?eT9 zHh)KeGTEJ*k}l+GAj2y)>1bEd2Ylf^vw~JfS($=0hT;!&f!5!$)3Xv5z!TvRk!-Cx zei2?@?Q7TjLq%m@%jwJET%_N(LOY`rf;c*MdF`AOAna>a9+rbqkd>Z`LG?s_-3=x% z_r{&IO5`aZ7z`hY$MCC@=qsR9q4{efi&|?ll7xQ_q><;;AH8aqGh&R~r6^v-2DC{f zr7?_5?G{VqF?ZH;#w`|T*%0IoE=mvq%=+I16pFdoy7Tti!ToOyA$u&9S~gBZnj?qN zr!JyYGcRnvHq_--;*;&v&ii#V=7v75O?rsj{7_(kn`use^IndV13ag{IfqyxJqrxl zJ&R>`_~q(ZN{+nUkJT(Bc}h*BSxvVL-mW!yqy#wwn!_W`=gK@->JUGPg1Rcu*C8Fr zF@-dWu61(iwd8GIZTPyAEd3%nI*U~ao3xa(Np?OkMtu7#{g64$irtn3q)n#ceErIk zYW>jFzbw>7;ailAZl(uAr6Pt&FC__6H%hT@10jVAg2RpEugk^Qw-?vQAV1M!}k8Z2eg2MZ-=*$rU^-5HMr6t~Hcv=iLv&uOV`*`Qvsx?}s|M8A3Q)Rp0 z1fPpq-!)K$VNp5kq}9wJ^rU~8why4+_oAEgz&ZNIss^%JxlJwQP1jhA+Wb8Yo@$RO zPCQ1ay!A*v#-HBc1|U^i&dfi4Bs3r?jsF?7^@z_tdbE?OL5q^LF zC7Cu7Q6wR!U`TW_pzuU4!e0-wGyXShu>?)3L6K!kbJEd}TpKpPA`K%2sUV<&lAxCS z$$dm>^UA&{JBjkiT5`r&OR|<7%TG72V7rh&wj|Asj>E}4=HY(nRk>bXCn9pEPvL&} zGQwEKnioBff@@8!NyB~S8rIj=#HSS@=(A8gb`Ti}UsR9u;86{++PRcecT7#hnJS$9 zm9i`!L{G4R?*;MG3l}xs3|Dcsw$viaHc@EiZszvv`$t?d7XR<#`>sd6lY$89f~t z787+}P8W{yB*Sf7&0rK`UU*uH2vo-eSGmoAFG(0mNsl3`4U5TiG7m?|+QG(nR@B@Axnb~FmAO^0t$I+-kVcKjRJ zXaacwst&QpJ-va@dxKC!IvMm1#0Cutek?$3?U7E#!h@f-i8`kkO%Qq?u)W7HlbL*% z-k{(YctnHid@c3|A*-jR|LAM5||y!ZM_C7^!; zH?88?_ojRri?$$~A-GL>*%IoExtlz&=Cgj`d7nnM8zOZL{Zxb>FPH#ANWoyTZ$h#n zpsuP|HYT1W&QtO12nnxImf_j@hr{_my2Y3Tx6=;GS6HC7mK$?75}^}veLb)CHek4b zYZWC0^N=5Ch3GlSn-A^z2y`naa9CJd`FzO%G^jA`=Xz)iKbHTHqR5gjJ!iu@B3?U2 zaom``UwBczXdDy{(#X{U=FdwDt2p%N0s#?5}nKw;`bZi+?!( z5fc6f4=6cr2MJgC8UN(aZ_bDAjg2MPAbKDc5S;;Pt2U87VjI6#&igBK#Fo7a37~H$ zcf~*Sl8LT$?iDq;S18);}GQ19$4&fK*+thour{88P1XO zjVRB7`8EaZ*d(EYDMI>lp?5UE@UC47l_&KhS&|&hC&!}zyfLe((A+DP>c2DiH{Yf` z{M2zj;XdWY3laJ7-uRJ-c7dKgk;D#fhUVVQaIgWUwhB3-PHs7>RPb{y3TXz!kL#vg zgxb_zz(-yr2Bq{Xz&c^{sh?@=N?{w=Y-iFf8`WgL7+Exo`k6qwBbtUBfcc1G%{Bq3{Z9#Zr6QFM3ma$+{1Nt=-2@`QGNbudqNAC!l}|y# z!euXdq*t5s8-%kIWQMngDiX+-(o#wi&)=jWAByAL64x)GArY8BKgQRjk_dXBU&kps zB+44Nu6-9)66GzW^ws(UQuqwkPC*oaobue*69C4NMtJ10&Y6~ryKN*oToX|i+}(_pg&FxFn=@50&!YzI}GZ{ z#ZwWA>=(X>uBkm#ic}^{8T}O*WTCyYQ;m;`U8(;hdY9D*PU{Y)Af=PLzDL@l!i5~g z&o#$e(=-v^$Y^|TAoOWeh|mZ`SK#yUGkA=qkffHErCxh(5zob=&ha?9wO#wKpC6eA zkHjRd@a=R|1t8I-pX9xNm3(e_V9gFg7B8|CbCrDTZx_mqw65g!saV*ln(n|ypd3 zgR?ntq4nZK$VZxyZ~MR79HWa$b7Af^G0%P-Sy}1PCMy5&WlvPTWE{&=!riYiwP8$5 zuvJU=E3jp36Z4=MYDpr0=cvOWniQZevvY;_{f-2i;LO8BeYaPF9OI`R1gnuUqc^FL zEUBub_Sn&bZ#jTVrD|+ml_{X}E#K65xRK@&i{Q3dzR<8^D3LGO_iY2|@7K zg@`vcLHz#sV#;Srm0#*obBr$+oX9kTkHJIx-Mh3cLcueyQ*pv6FL@7n_-~{EEl zx{7gFIsKT0*+)Vkx@JHPKqYc&uuQrL$FiK!dUJVvOGK|Cs_Un3;^n@Fvb$VIRv<1TX9=_LD-gY@g8(J;6qVehr<9nNi zHt7^QHXlok;;4W21OEhLPtoYJB!y*Z8DW@fUxb zv}3U!n4&;jIt<&Nd~%rlHh`K-_;I@*;GK5ZnwmfOw`J7d_>v`dH)J6kpIAC;**M*#%0^1rI?92hkBmCH@{KHJ zBiskgYs;DPhy}w$vWg`{kfM&%eT#s=VN%fsOVEvyse~cRj7w5cX0oDTlKg~|;RIt+9vHJz z9)OW46$jL17)6*U+t`RP1_=*s*>Uq|B(nW^ zZklU%Qr_VAXg$_h(~pyk2xHs|m3Zqm;W~S=JIRqtG5x-PiRrJ=Ex9QhZd`YgzgPB` z-MrjS1RiMw+l_n{oql#IUe$c-tR!2z3UeI0_i5SeD+&J$+}zpL6Y$Lncewsz3?#Yx zvkUTH>)XVze>VJc95B<~b!rgse(e|p92jik;zg>lMa^DP=|}T?$GzCXxkPxS+z>%( zQ$~JbO1QVX-dU(h;Yc0t*`czqU*`|7;lTCC3KrJ^=Rhn_ePnBCY~@Ocqqoc9v% z3M;Z*^#8p6?kVeivcSzDoIEeB`@>sEIKbwbTS*{bRiFWQ_-pk50`{`^oc?~hQPq32 zW4wMTaxcT&IaA+E=$@6x_2d>FVw%8}lc;IqDANqcLRM+Cab8Z;FyB~qPs%*Bv)ka_ ztacRA{S{}m-!viJY@Fr2ux7RdzYi0Ifa{@~v0YUMm(!;+L`f62KKE-r2#x&2#eC9r z?dwNOPQ>vK9_|ptPdBIE>)aR?KE9Z5u-P8Vm2RVIXJ|ZqagZV@q`jF`W9l{?9woGE zD+IIVHJw&1%NvN@%WSYAWa(DhE^GNSA+?yBQGP2V=R-uuQ-o)C0182I8Is-PrJNGAyfh^XaFtQaxn8=5BluToCBSke{lwz{`W~D*NpR=l5e1+I{sS-R};fZ*{9z@`>kSQX+|bR z;lvz^N}p{T5fE?n5tziqrI>@w%$bAT$%zM0`DImADh1-GC2OD%)={*QPloxz)=NtP zuAjWUed0gc_G)F#lmabsG{ScA-A@9|N&wDns#CxQ-H))JZnMFr{E+}uRPfbC#o@wX zEH@ZYez2&p+@kt*UUTrME>^@3JTT^x5x0hB(;j5s_q!edWaN07&=)b@CkDd*z#E`~ zBNp({Ppu&NUY*?*e9?sf+s3U;>O<)U zBCrDac*ZMHn(QYz^o@Ca?M4n`dpAyHdVFKo&Gf{>&SDY!Qmmlgx_C<$$ zDK&4{L=q#7EaZa`@%s6_b~AyNT)$D1Ev$Xx*i8p?uoj;!>uCqx-OrzHzvfX5o*Z)* z8qT^X1Ad0U$82qAXe67(#d3dZ3oY50vP+c=INAR2SYi{o$L6qE z>Zm$aRNk*80ZZD44~`C&xT_qxoLqYch-{eOpxxiAokZ2o_bs1lk2nz{wcM&!4!M5a z;p+5au;GP{B(rp#`5wNt7^Q~ev z#&?A~e2_l6`MID}e9r8^p!giW{~CnA ze>Je87jBo?wTv-}tL61|czhO)e9~@Gj$z6&W&UDgjnxwMj=(+b95r8KimAKbl?U^? zw{VOiLm#B1n6{Ici8EI3*0w24xvhtBYCqhr(5Iz#N+C;X^0Li#-_uIMznxD)Ug0D# zaU%}I(YbEK^_O!fVXC^m>P*{iIoKOlXTz+zj>BAW>Sz6gzxiJ?YscZy8DuN7n5v5g zsw_7=I_uw;dR-+7-12%!iZ;9s4Ft=KZv1wppPqD`DLO2b;~vEnVN#2f65x=VmR`Uy zFa=+6o+p1;MkLLjz-uCcIn%sd$cj)Ua#2o9^`&V&-3W!Bq$={ne1k{gC=s7&t zbgSQgAlH-Z>&z0=;H5Q~ctCFH7a#WAnz676Mu_1>+vqQj4vv#K7w>N4!oFxf=vvln z9Gyz7sa)Daj-O5@V;@;h+3vr1sFXzN9o4k!)z4JScAczk_mcIYoujl)fObI^e`g#$ z#yjQ@UcFBh@5SUi8eP&@;_vTB-<=3|;ZbuuT)j0hf|y^m2$0B9 zKW4XDkCOFysD4I*0`;EKcW1h<4kdWQ{P=}WmulAQf_DFGzAWC0pLoBMw^fZmhvNdo zadpl;%hY7gE6w0hnleJtU4z%eydKusr$FfCptY?_=yiNrzuhIYvN{W4h~HqN9h2Py z)LQL9@Rr4dW{^Q^?myHVesES+F`Op)yqEWfMdo$Mi zzuI==qThO`50|`Gf=(juU|jjmrRI`$JHZPIpN4_jeakVzh12FmejCCm?Gt~{NMBpq z%NZ&UQ4w_tr=T4kz5wjb@QFq3c0) z1hx+-e4N{RnoLJxh^f6{k*H6`P&mZHWI#lIXPIxN&m&_cOCAVxreAu3f;iQZZcOme zR9Q37M5IA1*d`rh!iD`gusiI_XOTAGWdGXZ76a zdFm%GS6sPAs5!|)-B|NoR(-t;fCyj6!~Glj_Jk5s$y30un7krp%7-c0ilFxVzOddV zn!0j|tdup93ND^O>Wk*Hy_uq^L>Vu*w3upY7N|<#Q%O5a|70(xSsV&<8ky-N=ku0)dJ1D*qoxG-4!uiIC9DV6Vugh>VYaig^W6OtUE zUhq|A+N*<1gFA&7p*n=T@42PHL-R7yPc{F5Q>9%HGkY`szl1jm7A-`&KS7SN@}!3 zs!TAKm(N>k&xtGqBG`evmV5{-2$OuKP(?>Q*QzV0UF~Z(otdb3sn>+=0F zk|JD{hA1h?N=IxvIfLA3F~&FrS^IL!7*8n-Ob0pp z$f45yQp)CpbQMwk26Oc!Rl$?J)GKqyuLMIH>x4COGsa?N!g6N&v=LV`UMk673ic2_ zK75EkG-XmShTM6yq%%UKg+y5`O4$oaQlGwUT@s9ZcwdcKk+O+0^bS2O_tsKK@9q1N zKpd8bv#7|d7XVnBCPgQ}77u2FT*i?1xAcmzO2Ml)xQhpiSYi%^A`DC`w=0w|^!*Wo zMFVt9=c5xiT8cn|2npz$TF@hFU}p8dXyT+7lj)ka(H0nt)Hwb?=PDB|)5%95DEt85 z6I1U|KqxETpc0$WZW+rAmzf_&8)=xE6Rm!h-NSmAk_pFOf zj;$^iiUkO2g9Ot|4EfbEEhlz>3a`d;NT{AVuK-e$1S!1~i&C5riF*c9gT%1qj|6qs z8)pJJ0(p?isMRmZ^kOzVq+ECM-by(!vkEoEJegx$Z6hIUIqO0583ys&Jq(NV?^F(N*fT8NR{Jl}gmP-< z)THG!(zRO+b1B|3#UQ6f(8z?GAbCG(_(>;xZznZk%7-FMQThIpde#YebB!P! zIs2x5Bpm665Xh$zh_}6S=jPsr7u;|muA72H5TAcxHL$wL`SnLtF0^+H3;Evz3@8nl z{h%Qr-4nq_vR74MGG$C(_8=q}**7}(<6(pHxxWXG*MSj%zt$SikFd+^bvrwT0$ewb z1fM_o41Fz`%{oB2>96!HzUSiegV&HOR_&r5Soj84@xBa6_7ITIXzc(WWl(>q5fQW! zI-=EKJ>2+Ef|*X|_~`z`WE;SE^|IZvZK?giTJ0Gi zI1gp^u#>+?vFzaO8razlpRVmWW-X#9KIe<+wY?6>k_v(kx-Vr`#{Cm5*ItK1_?_rv zIu@;}*Vk&Yh#rvt?Y-U)7`r^GnNG(VQ!US{kc6Y`c1q%(&TS7 zL>;@=TukGXdIvPQ0=7oFkUp9|8kjI7hV;iqE$DY|l4mZ9HtG1xf6|s^` zq{n+f5%5Y$3ZKi&czGlYxweyhe)7X_^ou==mx_|Ze#(BXe0D^T`+X7h<=Q3O*nb0^ zDDVE04v%xyQ0gb4olwubpbsyND8j+{Lbv467)2XHU6HlPT5nW&WcTY%Z`^O)70_?j zy#d>Yo6!Htf8Fr?+_CshKb(1I6=S{Edg2cDu4eYGM(SRUW-bQIo_4m`Gop%ptmyF5 ze#INTSGZJYjOb#rZM18FDB6>hb@=hpxw2;V@hkOW1*CUxZ_N)c5Nb{uT1*bWUDsI^ zG_qGz1FHLMt5Fw&ZWbsJ_o%ahU({UCw+{PdPvDcci`otJ*wun`-%SAG~GTXzDx7|mH(3mOci!? znDB0c^mi_>$~(f*7;Ix~VaDQO=Im|-HgjRIGjlaIHFh;-0XsOGF*!OrIGQ=TTD=dj z@v$4T^YWR1jZMIuoIJ*SrY0urCOpQxynLMIoaXGzjxKhlU`N}e^>^wpYUl;*9x}BK*lv{t=(oGnwHId>`|`bq)ol>7%1#>I+N^^#lEg zXC~_(dduB=4R+=BE+x?Met|kPq<0!NDQFmN@~LH}LT)4SDHi=DFconPr5aA#af>c0 z#)3RU2gdcXJ4`lXM4Xd@D~^KXIy|Cy zv}}}`7>fqYy`vm`2yBvW5_ajx!i#{eDhpmrimn~@*P_5i;9OX~ypu*dD?x$S>^G#Y z4YX7Rhkc9$LGGz4z;M^cdy4!Qa90=$v{W$7EJ4rH&#TJ(p#!gaL*sAP9d-Qsl61`R zvcI=@5$5Wob_dmnTuL_%ui$h(S`psRDGL*yq-}Gc2N1-}`_h2%yeKr9O*W@IRQ{Z% zmI{nT;XLzd6QI)rVId6+3#bkYWh{?sg(Oy~pUj_f2LH Date: Fri, 11 Jul 2025 15:38:46 +1000 Subject: [PATCH 23/38] - Fixed for savable mp logs --- Source/Client/Util/MpLog.cs | 7 +++++-- Source/Client/Util/SaveableMpLogs.cs | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Source/Client/Util/MpLog.cs b/Source/Client/Util/MpLog.cs index 7802dcfe..359c2ea3 100644 --- a/Source/Client/Util/MpLog.cs +++ b/Source/Client/Util/MpLog.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Multiplayer.Client.Util { @@ -7,23 +7,26 @@ public static class MpLog public static void Log(string msg) { Verse.Log.Message($"{Multiplayer.username} {TickPatch.Timer} {msg}"); + //SaveableMpLogs.AddLog("LOG", msg); } public static void Warn(string msg) { Verse.Log.Warning($"{Multiplayer.username} {TickPatch.Timer} {msg}"); + //SaveableMpLogs.AddLog("WARN", msg); } public static void Error(string msg) { Verse.Log.Error($"{Multiplayer.username} {TickPatch.Timer} {msg}"); + //SaveableMpLogs.AddLog("ERROR", msg); } [Conditional("DEBUG")] public static void Debug(string msg) { Verse.Log.Message($"{Multiplayer.username} {TickPatch.Timer} {msg}"); - SaveableMpLogs.AddLog(msg); + SaveableMpLogs.AddLog("DEBUG", msg); } } } diff --git a/Source/Client/Util/SaveableMpLogs.cs b/Source/Client/Util/SaveableMpLogs.cs index 69a838c9..81d68250 100644 --- a/Source/Client/Util/SaveableMpLogs.cs +++ b/Source/Client/Util/SaveableMpLogs.cs @@ -36,7 +36,7 @@ public static void InitMpLogs() public static void ResetMpLogs() => _currentLogFile = null; - public static void AddLog(string logText) + public static void AddLog(string type, string logText) { if (Multiplayer.Client == null) return; @@ -44,14 +44,14 @@ public static void AddLog(string logText) if (_currentLogFile == null) InitMpLogs(); - int ticks = Find.TickManager.ticksGameInt; + int ticks = Multiplayer.Client == null ? -1 : Find.TickManager?.TicksGame ?? -1; int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; try { using var stream = File.Open(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); using var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine($"[{ticks}] [{mapTicks}] {logText}"); + writer.WriteLine($"[{type}] [{ticks}] [{mapTicks}] {logText}"); } catch (Exception e) { From 65581925b9334eec493610fc218f9809dc08c274 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sat, 12 Jul 2025 16:38:32 +1000 Subject: [PATCH 24/38] vtr update - Now accounts for players disconnecting unexpectedly - No longer count a player as on a map if they have the world map open --- Source/Client/Patches/VTRSyncPatch.cs | 64 ++++++++++++++----- Source/Client/UI/PlayerCursors.cs | 2 +- .../Networking/State/ServerPlayingState.cs | 3 + Source/Common/PlayerManager.cs | 10 +++ Source/Common/ServerPlayer.cs | 3 + 5 files changed, 66 insertions(+), 16 deletions(-) diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index 5f76ef97..e502ac5b 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; using HarmonyLib; using Multiplayer.Client.Util; using Multiplayer.Common; -using RimWorld; -using UnityEngine; +using RimWorld.Planet; using Verse; namespace Multiplayer.Client.Patches @@ -25,15 +22,21 @@ static bool Prefix(Thing thing, ref int __result) private static int GetSynchronizedUpdateRate(Thing thing) => thing?.MapHeld?.AsyncTime()?.VTR ?? 15; } + static class VTRSync + { + public static int lastMovedToMap = -1; + public static int lastSentTick = -1; + + // Special identifier for world map (since it doesn't have a uniqueID like regular maps) + public const int WorldMapId = -2; + } + [HarmonyPatch(typeof(Game), nameof(Game.CurrentMap), MethodType.Setter)] static class MapSwitchPatch { - private static int lastSentFromMap = int.MaxValue; - private static int lastSentTick = -1; - static void Prefix(Map value) { - if (Multiplayer.Client == null) return; + if (Multiplayer.Client == null || Client.Multiplayer.session == null) return; try { @@ -41,16 +44,12 @@ static void Prefix(Map value) int newMap = value?.uniqueID ?? -1; int currentTick = Find.TickManager?.TicksGame ?? 0; - // Only send when multiplayer is ready - if (Multiplayer.Client == null || Client.Multiplayer.session == null) - return; - // If no change in map, do nothing if (previousMap == newMap) return; // Prevent duplicate commands for the same transition, but allow retry after a tick - if (lastSentFromMap == previousMap && currentTick == lastSentTick) + if (VTRSync.lastMovedToMap == previousMap && currentTick == VTRSync.lastSentTick) return; // Send map change command to server @@ -58,8 +57,8 @@ static void Prefix(Map value) Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(previousMap, newMap)); // Track this command to prevent duplicates - lastSentFromMap = previousMap; - lastSentTick = currentTick; + VTRSync.lastMovedToMap = newMap; + VTRSync.lastSentTick = currentTick; } catch (Exception ex) { @@ -67,4 +66,39 @@ static void Prefix(Map value) } } } + + [HarmonyPatch(typeof(WorldRendererUtility), nameof(WorldRendererUtility.CurrentWorldRenderMode), MethodType.Getter)] + static class WorldRenderModePatch + { + private static WorldRenderMode lastRenderMode = WorldRenderMode.None; + + static void Postfix(WorldRenderMode __result) + { + if (Multiplayer.Client == null) return; + + try + { + // Detect transition to world map (Planet mode) + if (__result == WorldRenderMode.Planet && lastRenderMode != WorldRenderMode.Planet) + { + if (VTRSync.lastMovedToMap != -1) + { + Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.lastMovedToMap, VTRSync.WorldMapId)); + } + } + // Detect transition away from world map + else if (__result != WorldRenderMode.Planet && lastRenderMode == WorldRenderMode.Planet) + { + int currentMapId = Find.CurrentMap?.uniqueID ?? -1; + Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.WorldMapId, currentMapId)); + } + + lastRenderMode = __result; + } + catch (Exception ex) + { + MpLog.Error($"WorldRenderModePatch error: {ex.Message}"); + } + } + } } diff --git a/Source/Client/UI/PlayerCursors.cs b/Source/Client/UI/PlayerCursors.cs index a0f8afce..081990f0 100644 --- a/Source/Client/UI/PlayerCursors.cs +++ b/Source/Client/UI/PlayerCursors.cs @@ -36,7 +36,7 @@ private void SendCursor() var writer = new ByteWriter(); writer.WriteByte(cursorSeq++); - if (Find.CurrentMap != null && !WorldRendererUtility.WorldRendered) + if (Find.CurrentMap != null && !WorldRendererUtility.WorldSelected) { writer.WriteByte((byte)Find.CurrentMap.Index); diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index 6f4c6ce2..70e7631d 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -115,6 +115,9 @@ public void HandleCursor(ByteReader data) byte seq = data.ReadByte(); byte map = data.ReadByte(); + + // Track the player's current map from cursor updates + Player.currentMap = map; writer.WriteInt32(Player.id); writer.WriteByte(seq); diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index 6a99ba97..7c061b42 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -80,6 +80,16 @@ public void SetDisconnected(ConnectionBase conn, MpDisconnectReason reason) if (player.hasJoined) { + // Handle unexpected disconnections by sending PlayerCount command + if (reason == MpDisconnectReason.ClientLeft || reason == MpDisconnectReason.NetFailed) + { + // Send PlayerCount command to remove player from their last known map + if (player.currentMap != -1) + { + byte[] playerCountData = ByteWriter.GetBytes(player.currentMap, -1); // previousMap: player's map, newMap: -1 (disconnected) + server.commands.Send(CommandType.PlayerCount, ScheduledCommand.NoFaction, ScheduledCommand.Global, playerCountData); + } + } // todo check player.IsPlaying? // todo FactionId might throw when called for not fully initialized players // if (Players.All(p => p.FactionId != player.FactionId)) diff --git a/Source/Common/ServerPlayer.cs b/Source/Common/ServerPlayer.cs index eba51c9d..0c658ce0 100644 --- a/Source/Common/ServerPlayer.cs +++ b/Source/Common/ServerPlayer.cs @@ -30,6 +30,9 @@ public class ServerPlayer : IChatSource public bool frozen; public int unfrozenAt; + + // Track which map the player is currently on (from cursor updates) + public int currentMap = -1; public string Username => conn.username; public int Latency => conn.Latency; From 3f61638311a2482332a9f4e92f524357c453032f Mon Sep 17 00:00:00 2001 From: Reznal Date: Sat, 12 Jul 2025 17:01:41 +1000 Subject: [PATCH 25/38] - Vtr applied to Projectiles and WorldObjects --- Source/Client/AsyncTime/AsyncTimeComp.cs | 2 +- Source/Client/Patches/VTRSyncPatch.cs | 39 +++++++++++++++++++--- Source/Client/UI/DebugPanel/StatusBadge.cs | 3 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index f5a96ad1..750ca5a2 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -86,7 +86,7 @@ public void SetDesiredTimeSpeed(TimeSpeed speed) public Queue cmds = new(); public int CurrentPlayerCount { get; private set; } = 0; - public int VTR => CurrentPlayerCount > 0 ? 1 : 15; + public int VTR => CurrentPlayerCount > 0 ? VTRSync.MinimumVtr : VTRSync.MaximumVtr; public AsyncTimeComp(Map map) { diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index e502ac5b..24a4811d 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -15,20 +15,49 @@ static bool Prefix(Thing thing, ref int __result) if (Multiplayer.Client == null) return true; - __result = GetSynchronizedUpdateRate(thing); + __result = VTRSync.GetSynchronizedUpdateRate(thing); return false; } + } + + [HarmonyPatch(typeof(Projectile), nameof(Projectile.UpdateRateTicks), MethodType.Getter)] + public static class VtrSyncProjectilePatch + { + static bool Prefix(ref int __result, Projectile __instance) + { + if (Multiplayer.Client == null) + return true; - private static int GetSynchronizedUpdateRate(Thing thing) => thing?.MapHeld?.AsyncTime()?.VTR ?? 15; + __result = __instance.Spawned ? VTRSync.GetSynchronizedUpdateRate(__instance) : VTRSync.MaximumVtr; + return false; + } + } + + [HarmonyPatch(typeof(WorldObject), nameof(WorldObject.UpdateRateTicks), MethodType.Getter)] + public static class VtrSyncWorldObjectPatch + { + static bool Prefix(ref int __result, WorldObject __instance) + { + if (Multiplayer.Client == null) + return true; + + __result = 15; + return false; + } } static class VTRSync { - public static int lastMovedToMap = -1; - public static int lastSentTick = -1; - // Special identifier for world map (since it doesn't have a uniqueID like regular maps) public const int WorldMapId = -2; + public static int lastMovedToMap = -1; + public static int lastSentTick = -1; + + // Vtr rates + public const int MaximumVtr = 15; + public const int MinimumVtr = 1; + + public static int GetSynchronizedUpdateRate(Thing thing) => thing?.MapHeld?.AsyncTime()?.VTR ?? VTRSync.MaximumVtr; } [HarmonyPatch(typeof(Game), nameof(Game.CurrentMap), MethodType.Setter)] diff --git a/Source/Client/UI/DebugPanel/StatusBadge.cs b/Source/Client/UI/DebugPanel/StatusBadge.cs index 8c54f1a4..9c382b49 100644 --- a/Source/Client/UI/DebugPanel/StatusBadge.cs +++ b/Source/Client/UI/DebugPanel/StatusBadge.cs @@ -1,3 +1,4 @@ +using Multiplayer.Client.Patches; using UnityEngine; using Verse; @@ -47,7 +48,7 @@ public static StatusBadge GetTickStatus() public static StatusBadge GetVtrStatus() { - int rate = Find.CurrentMap?.AsyncTime()?.VTR ?? 15; + int rate = Find.CurrentMap?.AsyncTime()?.VTR ?? VTRSync.MaximumVtr; return new StatusBadge("V", rate == 15 ? Color.red : Color.green, rate.ToString(), $"Variable Tick Rate: Things update every {rate} tick(s)"); } From 0e4f2f183d7ea0ec41acbd6135d7580d5257314f Mon Sep 17 00:00:00 2001 From: Reznal Date: Sat, 12 Jul 2025 17:38:32 +1000 Subject: [PATCH 26/38] - Added extra error handling to SaveableMPLogs - Fixed a remaining WorldRendered - Added extra error handlint to SyncDebugPanel --- Source/Client/AsyncTime/AsyncTimePatches.cs | 2 +- Source/Client/UI/DebugPanel/SyncDebugPanel.cs | 163 +++++++++++------ Source/Client/Util/SaveableMpLogs.cs | 172 ++++++++++++------ 3 files changed, 221 insertions(+), 116 deletions(-) diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index a5f135b0..e2a360d1 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -147,7 +147,7 @@ static class TickManagerPausedPatch static void Postfix(ref bool __result) { if (Multiplayer.Client == null) return; - if (WorldRendererUtility.WorldRendered) return; + if (WorldRendererUtility.WorldSelected) return; if (FactionCreator.generatingMap) return; var asyncTime = Find.CurrentMap.AsyncTime(); diff --git a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs index ae6ee9d6..2587a7af 100644 --- a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs +++ b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs @@ -241,20 +241,27 @@ private static float DrawRngStatesSection(float x, float y, float width) { List lines; - if (Find.CurrentMap?.AsyncTime() != null) + try { - AsyncTimeComp async = Find.CurrentMap.AsyncTime(); + AsyncTimeComp async = Find.CurrentMap?.AsyncTime(); AsyncTime.AsyncWorldTimeComp worldAsync = Multiplayer.AsyncWorldTime; - lines = [ - new("Current Map:", GetRngStatesString(async.randState), Color.white), - new("World RNG:", GetRngStatesString(worldAsync.randState), Color.white), - new("Round Mode:", $"{RoundMode.GetCurrentRoundMode()}", Color.white) - ]; + if (async != null && worldAsync != null) + { + lines = [ + new("Current Map:", GetRngStatesString(async.randState), Color.white), + new("World RNG:", GetRngStatesString(worldAsync.randState), Color.white), + new("Round Mode:", $"{RoundMode.GetCurrentRoundMode()}", Color.white) + ]; + } + else + { + lines = [new("RNG States:", "No async time available", Color.gray)]; + } } - else + catch (Exception ex) { - lines = [new("RNG States:", "No current map", Color.gray)]; + lines = [new("RNG States:", $"Error: {ex.Message}", Color.red)]; } return DrawSection(x, y, width, new("RNG STATES", lines.ToArray())); @@ -324,17 +331,25 @@ private static float DrawNetworkSyncSection(float x, float y, float width) /// private static float DrawCoreSystemSection(float x, float y, float width) { - DebugLine[] coreLines = [ - new("Faction Stack:", $"{FactionContext.stack.Count}", Color.white), - new("Player Faction:", $"{Faction.OfPlayer.loadID}", Color.white), - new("Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID}", Color.white), - new("Next Thing ID:", $"{Find.UniqueIDsManager.nextThingID}", Color.white), - new("Next Job ID:", $"{Find.UniqueIDsManager.nextJobID}", Color.white), - new("Game Ticks:", $"{Find.TickManager.TicksGame}", Color.white), - new("Time Speed:", $"{Find.TickManager.CurTimeSpeed}", Color.white) - ]; + try + { + DebugLine[] coreLines = [ + new("Faction Stack:", $"{FactionContext.stack?.Count ?? 0}", Color.white), + new("Player Faction:", $"{Faction.OfPlayer?.loadID ?? -1}", Color.white), + new("Real Player:", $"{Multiplayer.RealPlayerFaction?.loadID ?? -1}", Color.white), + new("Next Thing ID:", $"{Find.UniqueIDsManager?.nextThingID ?? -1}", Color.white), + new("Next Job ID:", $"{Find.UniqueIDsManager?.nextJobID ?? -1}", Color.white), + new("Game Ticks:", $"{Find.TickManager?.TicksGame ?? -1}", Color.white), + new("Time Speed:", $"{Find.TickManager?.CurTimeSpeed ?? TimeSpeed.Paused}", Color.white) + ]; - return DrawSection(x, y, width, new("CORE SYSTEM", coreLines)); + return DrawSection(x, y, width, new("CORE SYSTEM", coreLines)); + } + catch (Exception ex) + { + DebugLine[] errorLines = [new("Core System:", $"Error: {ex.Message}", Color.red)]; + return DrawSection(x, y, width, new("CORE SYSTEM", errorLines)); + } } /// @@ -342,18 +357,26 @@ private static float DrawCoreSystemSection(float x, float y, float width) /// private static float DrawTimingSyncSection(float x, float y, float width) { - int timerLag = TickPatch.tickUntil - TickPatch.Timer; - Color lagColor = StatusBadge.GetPerformanceColor(timerLag, 15, 30); - - DebugLine[] timingLines = [ - new("Timer Lag:", $"{timerLag}", lagColor), - new("Timer:", $"{TickPatch.Timer}", Color.white), - new("Tick Until:", $"{TickPatch.tickUntil}", Color.white), - new("Raw Tick Timer:", $"{TickPatch.tickTimer.ElapsedMilliseconds}ms", Color.white), - new("World Settlements:", $"{Find.World.worldObjects.settlements.Count}", Color.white) - ]; + try + { + int timerLag = TickPatch.tickUntil - TickPatch.Timer; + Color lagColor = StatusBadge.GetPerformanceColor(timerLag, 15, 30); + + DebugLine[] timingLines = [ + new("Timer Lag:", $"{timerLag}", lagColor), + new("Timer:", $"{TickPatch.Timer}", Color.white), + new("Tick Until:", $"{TickPatch.tickUntil}", Color.white), + new("Raw Tick Timer:", $"{TickPatch.tickTimer?.ElapsedMilliseconds ?? 0}ms", Color.white), + new("World Settlements:", $"{Find.World?.worldObjects?.settlements?.Count ?? 0}", Color.white) + ]; - return DrawSection(x, y, width, new("TIMING & SYNC", timingLines)); + return DrawSection(x, y, width, new("TIMING & SYNC", timingLines)); + } + catch (Exception ex) + { + DebugLine[] errorLines = [new("Timing Sync:", $"Error: {ex.Message}", Color.red)]; + return DrawSection(x, y, width, new("TIMING & SYNC", errorLines)); + } } /// @@ -361,17 +384,25 @@ private static float DrawTimingSyncSection(float x, float y, float width) /// private static float DrawGameStateSection(float x, float y, float width) { - AsyncTimeComp async = Find.CurrentMap.AsyncTime(); - - DebugLine[] gameStateLines = [ - new("Classic Mode:", $"{Find.IdeoManager.classicMode}", Color.white), - new("Client Opinions:", $"{Multiplayer.game.sync.knownClientOpinions.Count}", Color.white), - new("First Opinion Tick:", $"{Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick}", Color.white), - new("Map Ticks:", $"{async.mapTicks}", Color.white), - new("Frozen At:", $"{TickPatch.frozenAt}", Color.white) - ]; + try + { + AsyncTimeComp async = Find.CurrentMap?.AsyncTime(); + + DebugLine[] gameStateLines = [ + new("Classic Mode:", $"{Find.IdeoManager?.classicMode ?? false}", Color.white), + new("Client Opinions:", $"{Multiplayer.game?.sync?.knownClientOpinions?.Count ?? 0}", Color.white), + new("First Opinion Tick:", $"{Multiplayer.game?.sync?.knownClientOpinions?.FirstOrDefault()?.startTick ?? -1}", Color.white), + new("Map Ticks:", $"{async?.mapTicks ?? -1}", Color.white), + new("Frozen At:", $"{TickPatch.frozenAt}", Color.white) + ]; - return DrawSection(x, y, width, new("GAME STATE", gameStateLines)); + return DrawSection(x, y, width, new("GAME STATE", gameStateLines)); + } + catch (Exception ex) + { + DebugLine[] errorLines = [new("Game State:", $"Error: {ex.Message}", Color.red)]; + return DrawSection(x, y, width, new("GAME STATE", errorLines)); + } } /// @@ -394,17 +425,25 @@ private static float DrawRngDebugSection(float x, float y, float width) /// private static float DrawCommandSyncSection(float x, float y, float width) { - AsyncTimeComp async = Find.CurrentMap.AsyncTime(); - - DebugLine[] commandLines = [ - new("Async Commands:", $"{async.cmds.Count}", Color.white), - new("World Commands:", $"{Multiplayer.AsyncWorldTime.cmds.Count}", Color.white), - new("Force Normal Speed:", $"{async.slower.forceNormalSpeedUntil}", Color.white), - new("Async Time Status:", $"{Multiplayer.GameComp.asyncTime}", Color.white), - new("Buffered Changes:", $"{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}", Color.white) - ]; + try + { + AsyncTimeComp async = Find.CurrentMap?.AsyncTime(); + + DebugLine[] commandLines = [ + new("Async Commands:", $"{async?.cmds?.Count ?? 0}", Color.white), + new("World Commands:", $"{Multiplayer.AsyncWorldTime?.cmds?.Count ?? 0}", Color.white), + new("Force Normal Speed:", $"{async?.slower?.forceNormalSpeedUntil ?? -1}", Color.white), + new("Async Time Status:", $"{Multiplayer.GameComp?.asyncTime ?? false}", Color.white), + new("Buffered Changes:", $"{SyncFieldUtil.bufferedChanges?.Sum(kv => kv.Value?.Count ?? 0) ?? 0}", Color.white) + ]; - return DrawSection(x, y, width, new("COMMAND & SYNC", commandLines)); + return DrawSection(x, y, width, new("COMMAND & SYNC", commandLines)); + } + catch (Exception ex) + { + DebugLine[] errorLines = [new("Command Sync:", $"Error: {ex.Message}", Color.red)]; + return DrawSection(x, y, width, new("COMMAND & SYNC", errorLines)); + } } /// @@ -430,15 +469,23 @@ private static float DrawMemoryPerformanceSection(float x, float y, float width) /// private static float DrawMapManagementSection(float x, float y, float width) { - DebugLine[] mapLines = [ - new("Haul Destinations:", $"{Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count}", Color.white), - new("Designations:", $"{Find.CurrentMap.designationManager.designationsByDef.Count}", Color.white), - new("Haulable Items:", $"{Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}", Color.white), - new("Mining Designations:", $"{Find.CurrentMap.designationManager.SpawnedDesignationsOfDef(DesignationDefOf.Mine).Count()}", Color.white), - new("First Ideology ID:", $"{Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}", Color.white) - ]; + try + { + DebugLine[] mapLines = [ + new("Haul Destinations:", $"{Find.CurrentMap?.haulDestinationManager?.AllHaulDestinationsListForReading?.Count ?? 0}", Color.white), + new("Designations:", $"{Find.CurrentMap?.designationManager?.designationsByDef?.Count ?? 0}", Color.white), + new("Haulable Items:", $"{Find.CurrentMap?.listerHaulables?.ThingsPotentiallyNeedingHauling()?.Count ?? 0}", Color.white), + new("Mining Designations:", $"{Find.CurrentMap?.designationManager?.SpawnedDesignationsOfDef(DesignationDefOf.Mine)?.Count() ?? 0}", Color.white), + new("First Ideology ID:", $"{Find.IdeoManager?.IdeosInViewOrder?.FirstOrDefault()?.id ?? -1}", Color.white) + ]; - return DrawSection(x, y, width, new("MAP MANAGEMENT", mapLines)); + return DrawSection(x, y, width, new("MAP MANAGEMENT", mapLines)); + } + catch (Exception ex) + { + DebugLine[] errorLines = [new("Map Management:", $"Error: {ex.Message}", Color.red)]; + return DrawSection(x, y, width, new("MAP MANAGEMENT", errorLines)); + } } // Helper methods for UI drawing diff --git a/Source/Client/Util/SaveableMpLogs.cs b/Source/Client/Util/SaveableMpLogs.cs index 81d68250..3a658e05 100644 --- a/Source/Client/Util/SaveableMpLogs.cs +++ b/Source/Client/Util/SaveableMpLogs.cs @@ -20,17 +20,24 @@ public class SaveableMpLogs public static void InitMpLogs() { - _currentLogFile = FindFileNameForNextFile(); - try { + _currentLogFile = FindFileNameForNextFile(); + + if (string.IsNullOrEmpty(_currentLogFile)) + { + Log.Warning("SaveableMpLogs: Could not determine log file name, skipping log initialization"); + return; + } + using var stream = File.Open(_currentLogFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); using var writer = new StreamWriter(stream, Encoding.UTF8); writer.WriteLine(GetLogDetails()); } catch (Exception e) { - Log.Error($"Exception writing initial log info: {e}"); + Log.Error($"SaveableMpLogs: Exception writing initial log info: {e}"); + _currentLogFile = null; // Reset on error } } @@ -38,88 +45,139 @@ public static void InitMpLogs() public static void AddLog(string type, string logText) { - if (Multiplayer.Client == null) - return; + try + { + if (Multiplayer.Client == null) + return; - if (_currentLogFile == null) - InitMpLogs(); + if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(logText)) + { + Log.Warning("SaveableMpLogs: Invalid log parameters, skipping log entry"); + return; + } - int ticks = Multiplayer.Client == null ? -1 : Find.TickManager?.TicksGame ?? -1; - int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; + if (_currentLogFile == null) + { + InitMpLogs(); + if (_currentLogFile == null) // Still null after init attempt + return; + } + + int ticks = Multiplayer.Client == null ? -1 : Find.TickManager?.TicksGame ?? -1; + int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; - try - { using var stream = File.Open(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); using var writer = new StreamWriter(stream, Encoding.UTF8); writer.WriteLine($"[{type}] [{ticks}] [{mapTicks}] {logText}"); } catch (Exception e) { - Log.Error($"Exception writing log info: {e}"); + Log.Error($"SaveableMpLogs: Exception writing log info: {e}"); + // Don't reset _currentLogFile here as it might be a temporary file system issue } } private static string GetLogDetails() { - var logDetails = new StringBuilder() - .AppendLine($"Multiplayer Log - {DateTime.Now}") - .AppendLine("\n###Version Data###") - .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") - .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") - .AppendLine("\n###Debug Options###") - .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") - .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp.debugMode}") - .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") - .AppendLine("\n###Server Info###") - .AppendLine($"Async time active|||{Multiplayer.GameComp.asyncTime}") - .AppendLine($"Multifaction active|||{Multiplayer.GameComp.multifaction}") - .AppendLine("\n###OS Info###") - .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") - .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}") - .AppendLine("\n======================================================") - .AppendLine("###Log Start###") - .AppendLine("======================================================"); - return logDetails.ToString(); + try + { + var logDetails = new StringBuilder() + .AppendLine($"Multiplayer Log - {DateTime.Now}") + .AppendLine("\n###Version Data###") + .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") + .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") + .AppendLine("\n###Debug Options###") + .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") + .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp?.debugMode ?? false}") + .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") + .AppendLine("\n###Server Info###") + .AppendLine($"Async time active|||{Multiplayer.GameComp?.asyncTime ?? false}") + .AppendLine($"Multifaction active|||{Multiplayer.GameComp?.multifaction ?? false}") + .AppendLine("\n###OS Info###") + .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") + .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}") + .AppendLine("\n======================================================") + .AppendLine("###Log Start###") + .AppendLine("======================================================"); + return logDetails.ToString(); + } + catch (Exception e) + { + Log.Error($"SaveableMpLogs: Exception getting log details: {e}"); + return $"Multiplayer Log - {DateTime.Now}\nError getting full log details: {e.Message}\n======================================================"; + } } private static string FindFileNameForNextFile() { - // Get player directory - string directory = Path.Combine(Multiplayer.MpLogsDir);//, Multiplayer.username); - - // Ensure the directory exists - Directory.CreateDirectory(directory); - - // Get all existing logs - FileInfo[] files = new DirectoryInfo(directory).GetFiles($"{FilePrefix}*{FileExtension}"); - - // Delete any pushing us over the limit, and reserve room for one more - if (files.Length > MaxFiles - 1) - files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); - - // Find the current max number - int max = 0; - foreach (FileInfo file in files) + try { - // Get name without extension and prefix - string parsedName = Path.GetFileNameWithoutExtension(file.Name)[FilePrefix.Length..]; - - // Try to parse the number and update max if it's greater - if (int.TryParse(parsedName, out int result) && result > max) - max = result; + // Get player directory + string directory = Path.Combine(Multiplayer.MpLogsDir); + + // Ensure the directory exists + Directory.CreateDirectory(directory); + + // Get all existing logs + FileInfo[] files = new DirectoryInfo(directory).GetFiles($"{FilePrefix}*{FileExtension}"); + + // Delete any pushing us over the limit, and reserve room for one more + if (files.Length > MaxFiles - 1) + { + try + { + files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); + } + catch (Exception e) + { + Log.Warning($"SaveableMpLogs: Exception cleaning up old log files: {e}"); + } + } + + // Find the current max number + int max = 0; + foreach (FileInfo file in files) + { + try + { + // Get name without extension and prefix + string fileName = Path.GetFileNameWithoutExtension(file.Name); + if (fileName.Length > FilePrefix.Length) + { + string parsedName = fileName[FilePrefix.Length..]; + + // Try to parse the number and update max if it's greater + if (int.TryParse(parsedName, out int result) && result > max) + max = result; + } + } + catch (Exception e) + { + Log.Warning($"SaveableMpLogs: Exception processing file {file.Name}: {e}"); + } + } + + return Path.Combine(directory, $"{FilePrefix}{max + 1:00}{FileExtension}"); + } + catch (Exception e) + { + Log.Error($"SaveableMpLogs: Exception finding log file name: {e}"); + return null; } - - return Path.Combine(directory, $"{FilePrefix}{max + 1:00}{FileExtension}"); } private static void DeleteFileSilent(FileInfo file) { try { - file.Delete(); + if (file?.Exists == true) + { + file.Delete(); + } } - catch (IOException) + catch (Exception) { + // Silently ignore all exceptions when deleting old log files } } } From d66e69f17395873aa9a5c930e93b2640cd6cb76b Mon Sep 17 00:00:00 2001 From: Reznal Date: Sat, 12 Jul 2025 17:43:59 +1000 Subject: [PATCH 27/38] Removed some artifacts from the merge --- Source/Client/Debug/DebugSync.cs | 4 ---- Source/Client/Factions/FactionContextSetters.cs | 1 - Source/Client/Factions/FactionRepeater.cs | 3 --- Source/Common/Native.cs | 1 - 4 files changed, 9 deletions(-) diff --git a/Source/Client/Debug/DebugSync.cs b/Source/Client/Debug/DebugSync.cs index b307fa85..b07edb0e 100644 --- a/Source/Client/Debug/DebugSync.cs +++ b/Source/Client/Debug/DebugSync.cs @@ -3,15 +3,11 @@ using HarmonyLib; using LudeonTK; using Multiplayer.Client.Util; -using Multiplayer.Client.Util; using Multiplayer.Common; - using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; -using static HarmonyLib.AccessTools; -using static HarmonyLib.AccessTools; namespace Multiplayer.Client { diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs index 0f21170e..46366fdd 100644 --- a/Source/Client/Factions/FactionContextSetters.cs +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Generic; using HarmonyLib; using RimWorld; using RimWorld.Planet; diff --git a/Source/Client/Factions/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs index 06394921..7fd1a811 100644 --- a/Source/Client/Factions/FactionRepeater.cs +++ b/Source/Client/Factions/FactionRepeater.cs @@ -3,9 +3,6 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; using HarmonyLib; using Multiplayer.Client.Factions; using RimWorld; diff --git a/Source/Common/Native.cs b/Source/Common/Native.cs index f578e3d6..c04d0bef 100644 --- a/Source/Common/Native.cs +++ b/Source/Common/Native.cs @@ -4,7 +4,6 @@ using System.Runtime.InteropServices; using System.Threading; using HarmonyLib; -using Verse; namespace Multiplayer.Client { From 6c1c00500ca4b83fcfcc854e1ae8b96568f18b80 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 13 Jul 2025 15:47:20 +1000 Subject: [PATCH 28/38] bringing up to upsteam dev + prs --- Source/Client/Debug/DebugSync.cs | 2 ++ Source/Client/Factions/MultifactionPatches.cs | 2 +- Source/Client/Patches/VTRSyncPatch.cs | 9 +++++---- Source/Client/Syncing/Game/SyncActions.cs | 2 +- Source/Client/UI/IngameDebug.cs | 4 +--- Source/Client/UI/PlayerCursors.cs | 2 +- Source/Client/Util/MpLog.cs | 5 +---- Source/Common/Common.csproj | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Source/Client/Debug/DebugSync.cs b/Source/Client/Debug/DebugSync.cs index b07edb0e..2c77545d 100644 --- a/Source/Client/Debug/DebugSync.cs +++ b/Source/Client/Debug/DebugSync.cs @@ -4,10 +4,12 @@ using LudeonTK; using Multiplayer.Client.Util; using Multiplayer.Common; + using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; +using static HarmonyLib.AccessTools; namespace Multiplayer.Client { diff --git a/Source/Client/Factions/MultifactionPatches.cs b/Source/Client/Factions/MultifactionPatches.cs index 239e4c0c..3b09155e 100644 --- a/Source/Client/Factions/MultifactionPatches.cs +++ b/Source/Client/Factions/MultifactionPatches.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index 24a4811d..b6a522e3 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -1,8 +1,9 @@ -using System; using HarmonyLib; +using Multiplayer.Client.Patches; using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld.Planet; +using System; using Verse; namespace Multiplayer.Client.Patches @@ -41,7 +42,7 @@ static bool Prefix(ref int __result, WorldObject __instance) if (Multiplayer.Client == null) return true; - __result = 15; + __result = VTRSync.MaximumVtr; return false; } } @@ -121,7 +122,7 @@ static void Postfix(WorldRenderMode __result) int currentMapId = Find.CurrentMap?.uniqueID ?? -1; Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.WorldMapId, currentMapId)); } - + lastRenderMode = __result; } catch (Exception ex) @@ -130,4 +131,4 @@ static void Postfix(WorldRenderMode __result) } } } -} +} diff --git a/Source/Client/Syncing/Game/SyncActions.cs b/Source/Client/Syncing/Game/SyncActions.cs index 9111372e..99ac9326 100644 --- a/Source/Client/Syncing/Game/SyncActions.cs +++ b/Source/Client/Syncing/Game/SyncActions.cs @@ -1,4 +1,4 @@ -using RimWorld; +using RimWorld; using RimWorld.Planet; using System; using System.Collections.Generic; diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs index 72a6bc5d..c2f00fd2 100644 --- a/Source/Client/UI/IngameDebug.cs +++ b/Source/Client/UI/IngameDebug.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Text; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; @@ -103,7 +102,6 @@ internal static void DoDebugPrintout() // RandGetValuePatch.tracesThistick = 0; } - internal static float DoDevInfo(float y) { float x = UI.screenWidth - BtnWidth - BtnMargin; diff --git a/Source/Client/UI/PlayerCursors.cs b/Source/Client/UI/PlayerCursors.cs index 424e146b..6c23ce55 100644 --- a/Source/Client/UI/PlayerCursors.cs +++ b/Source/Client/UI/PlayerCursors.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Multiplayer.Common; using RimWorld.Planet; diff --git a/Source/Client/Util/MpLog.cs b/Source/Client/Util/MpLog.cs index 359c2ea3..05013ecd 100644 --- a/Source/Client/Util/MpLog.cs +++ b/Source/Client/Util/MpLog.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Multiplayer.Client.Util { @@ -7,19 +7,16 @@ public static class MpLog public static void Log(string msg) { Verse.Log.Message($"{Multiplayer.username} {TickPatch.Timer} {msg}"); - //SaveableMpLogs.AddLog("LOG", msg); } public static void Warn(string msg) { Verse.Log.Warning($"{Multiplayer.username} {TickPatch.Timer} {msg}"); - //SaveableMpLogs.AddLog("WARN", msg); } public static void Error(string msg) { Verse.Log.Error($"{Multiplayer.username} {TickPatch.Timer} {msg}"); - //SaveableMpLogs.AddLog("ERROR", msg); } [Conditional("DEBUG")] diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj index 061704c8..ca39c53c 100644 --- a/Source/Common/Common.csproj +++ b/Source/Common/Common.csproj @@ -16,7 +16,7 @@ - + From ba125204dd6f8344544a8fd1f1e68f01bb8ced57 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 13 Jul 2025 16:33:02 +1000 Subject: [PATCH 29/38] - Fixed pings --- Source/Client/Networking/State/ClientPlayingState.cs | 3 ++- Source/Client/UI/PingInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Client/Networking/State/ClientPlayingState.cs b/Source/Client/Networking/State/ClientPlayingState.cs index 8ed57f0a..030569d1 100644 --- a/Source/Client/Networking/State/ClientPlayingState.cs +++ b/Source/Client/Networking/State/ClientPlayingState.cs @@ -1,6 +1,7 @@ using Ionic.Zlib; using Multiplayer.Common; using RimWorld; +using RimWorld.Planet; using System; using System.Collections.Generic; using UnityEngine; @@ -173,7 +174,7 @@ public void HandlePing(ByteReader data) { int player = data.ReadInt32(); int map = data.ReadInt32(); - int planetTile = data.ReadInt32(); + PlanetTile planetTile = new(data.ReadInt32(), data.ReadInt32()); var loc = new Vector3(data.ReadFloat(), data.ReadFloat(), data.ReadFloat()); Session.locationPings.ReceivePing(player, map, planetTile, loc); diff --git a/Source/Client/UI/PingInfo.cs b/Source/Client/UI/PingInfo.cs index cbeb6d01..3809575d 100644 --- a/Source/Client/UI/PingInfo.cs +++ b/Source/Client/UI/PingInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using Multiplayer.Client.Util; using RimWorld.Planet; using UnityEngine; @@ -10,7 +10,7 @@ public class PingInfo { public int player; public int mapId; // Map id or -1 for planet - public int planetTile; + public PlanetTile planetTile; public Vector3 mapLoc; public PlayerInfo PlayerInfo => Multiplayer.session.GetPlayerInfo(player); From 5f29ea504d9a410edd7a3a68ebbb5ca80e64523d Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 13 Jul 2025 22:10:19 +1000 Subject: [PATCH 30/38] - Added Deterministic HashCombineInt for 3,5,6,7 and 8 combinations. - Patched all version of System.HashCode.Combine which will cover all future functions of this to. --- Source/Client/Patches/HashCodes.cs | 149 ++++++++++++++++++++++++ Source/Client/Util/DeterministicHash.cs | 99 ++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Source/Client/Util/DeterministicHash.cs diff --git a/Source/Client/Patches/HashCodes.cs b/Source/Client/Patches/HashCodes.cs index 9daa7bca..73cb0272 100644 --- a/Source/Client/Patches/HashCodes.cs +++ b/Source/Client/Patches/HashCodes.cs @@ -1,7 +1,10 @@ using HarmonyLib; +using Multiplayer.Client.Util; using RimWorld; using RimWorld.Planet; +using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using Verse; @@ -62,4 +65,150 @@ static void Postfix(Tradeable __instance, ref int __result) __result = RuntimeHelpers.GetHashCode(__instance); } } + + // Have created a patch that will handle 2-8 System.HashCode.Combine functions. + // TODO: Check the following: + // TileQueryParams.GetHashCode + // UnmanagedGridTraverseParams.GetHashCode + // MapGridRequest.GetHashCode + // SimplifiedPastureNutritionSimulator.GetHashCode + // FieldAliasCache.GetHashCode + static class HashCodeDetours + { + private static readonly Type HashCodeType = typeof(string).Assembly.GetType("System.HashCode"); + + internal static MethodBase GetCombineIntMethod(int numOfInts) + { + Type[] intTypes = Enumerable.Repeat(typeof(int), numOfInts).ToArray(); + + // Look for a pre-compiled int,int,… overload (only exists on some runtimes) + MethodInfo method = HashCodeType.GetMethod("Combine", BindingFlags.Public | BindingFlags.Static, binder: null, types: intTypes, modifiers: null); + if (method != null) + return method; // good – no generics involved + + // Fall back to the generic definition and close it for + MethodInfo genericMethod = HashCodeType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(x => + x.Name == "Combine" && + x.IsGenericMethodDefinition && + x.GetGenericArguments().Length == numOfInts); + + return genericMethod.MakeGenericMethod(intTypes); + } + } + + // ─── PATCH 2 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine2Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(2); + + static bool Prefix(int value1, int value2, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = Gen.HashCombineInt(value1, value2); + return false; + } + } + + // ─── PATCH 3 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine3Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(3); + + static bool Prefix(int value1, int value2, int value3, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = DeterministicHash.HashCombineInt(value1, value2, value3); + return false; + } + } + + // ─── PATCH 4 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine4Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(4); + + static bool Prefix(int value1, int value2, int value3, int value4, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = Gen.HashCombineInt(value1, value2, value3, value4); + return false; + } + } + + // ─── PATCH 5 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine5Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(5); + + static bool Prefix(int value1, int value2, int value3, int value4, int value5, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = DeterministicHash.HashCombineInt(value1, value2, value3, value4, value5); + return false; + } + } + + // ─── PATCH 6 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine6Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(6); + + static bool Prefix(int value1, int value2, int value3, int value4, int value5, int value6, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = DeterministicHash.HashCombineInt(value1, value2, value3, + value4, value5, value6); + return false; + } + } + + // ─── PATCH 7 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine7Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(7); + + static bool Prefix(int value1, int value2, int value3, int value4, int value5, int value6, int value7, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = DeterministicHash.HashCombineInt(value1, value2, value3, value4, + value5, value6, value7); + return false; + } + } + + // ─── PATCH 8 ints ───────────────────────────────────────── + [HarmonyPatch] + static class Combine8Patch + { + static MethodBase TargetMethod() => HashCodeDetours.GetCombineIntMethod(8); + + static bool Prefix(int value1, int value2, int value3, int value4, int value5, int value6, int value7, int value8, ref int __result) + { + if (Multiplayer.Client == null) + return true; + + __result = DeterministicHash.HashCombineInt(value1, value2, value3, value4, + value5, value6, value7, value8); + return false; + } + } } diff --git a/Source/Client/Util/DeterministicHash.cs b/Source/Client/Util/DeterministicHash.cs new file mode 100644 index 00000000..a8d9adf9 --- /dev/null +++ b/Source/Client/Util/DeterministicHash.cs @@ -0,0 +1,99 @@ +namespace Multiplayer.Client.Util +{ + public static class DeterministicHash + { + public const int DefaultSeed = 352654597; + public const int DefaultSeed2 = 1566083941; + + // 3-value combiner ────────────────────────────────────── + public static int HashCombineInt(int v1, int v2, int v3) + { + unchecked + { + int h = DefaultSeed; + h = ((h << 5) + h + (h >> 27)) ^ v1; + h = ((h << 5) + h + (h >> 27)) ^ v2; + h = ((h << 5) + h + (h >> 27)) ^ v3; + return h; + } + } + + // 5-value combiner ────────────────────────────────────── + public static int HashCombineInt(int v1, int v2, int v3, int v4, int v5) + { + unchecked + { + int h1 = DefaultSeed; + int h2 = h1; + + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v1; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v2; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v3; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v4; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v5; + + return h1 + h2 * DefaultSeed2; + } + } + + // 6-value combiner ────────────────────────────────────── + public static int HashCombineInt(int v1, int v2, int v3, int v4, int v5, int v6) + { + unchecked + { + int h1 = DefaultSeed; + int h2 = h1; + + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v1; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v2; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v3; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v4; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v5; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v6; + + return h1 + h2 * DefaultSeed2; + } + } + + // 7-value combiner ───────────────────────────────────────── + public static int HashCombineInt(int v1, int v2, int v3, int v4, int v5, int v6, int v7) + { + unchecked + { + int h1 = DefaultSeed; + int h2 = h1; + + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v1; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v2; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v3; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v4; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v5; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v6; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v7; + + return h1 + h2 * DefaultSeed2; + } + } + + // 8-value combiner ───────────────────────────────────────── + public static int HashCombineInt(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8) + { + unchecked + { + int h1 = DefaultSeed; + int h2 = h1; + + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v1; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v2; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v3; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v4; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v5; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v6; + h1 = ((h1 << 5) + h1 + (h1 >> 27)) ^ v7; + h2 = ((h2 << 5) + h2 + (h2 >> 27)) ^ v8; + + return h1 + h2 * DefaultSeed2; + } + } + } +} From 0ca31e62525a880f067dfbe780a264f491d81842 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 13 Jul 2025 23:05:15 +1000 Subject: [PATCH 31/38] Camping integreation --- .../Client/Factions/FactionContextSetters.cs | 21 +++++-- Source/Client/Factions/TileFactionContext.cs | 36 +++++++++++ .../Client/Persistent/CampCreationPatches.cs | 63 +++++++++++++++++++ .../Persistent/CaravanFormingPatches.cs | 44 ++++++------- .../Persistent/CaravanFormingSession.cs | 6 +- 5 files changed, 141 insertions(+), 29 deletions(-) create mode 100644 Source/Client/Factions/TileFactionContext.cs create mode 100644 Source/Client/Persistent/CampCreationPatches.cs diff --git a/Source/Client/Factions/FactionContextSetters.cs b/Source/Client/Factions/FactionContextSetters.cs index 46366fdd..3594d589 100644 --- a/Source/Client/Factions/FactionContextSetters.cs +++ b/Source/Client/Factions/FactionContextSetters.cs @@ -26,11 +26,13 @@ static class MapGenFactionPatch { static void Prefix(PlanetTile 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"); + MapParent mapParent = Find.WorldObjects.MapParentAt(tile); + Faction factionToSet = mapParent?.Faction ?? TileFactionContext.GetFactionForTile(tile); - FactionContext.Push(mapParent?.Faction); + if (Multiplayer.Client != null && factionToSet == null) + Log.Warning($"Couldn't set the faction context for map gen at {tile.tileId}: no world object and no stored faction."); + + FactionContext.Push(factionToSet); } static void Finalizer() @@ -116,3 +118,14 @@ static void Finalizer(Map __state) __state?.PopFaction(); } } + +// Clean up after map generation is complete +[HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] +static class CleanupTileFactionContext +{ + static void Finalizer(MapParent parent) + { + if (parent != null) + TileFactionContext.ClearTile(parent.Tile); + } +} diff --git a/Source/Client/Factions/TileFactionContext.cs b/Source/Client/Factions/TileFactionContext.cs new file mode 100644 index 00000000..5c35965e --- /dev/null +++ b/Source/Client/Factions/TileFactionContext.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using RimWorld; +using RimWorld.Planet; + +namespace Multiplayer.Client.Factions +{ + public static class TileFactionContext + { + private static readonly Dictionary tileFactions = []; + private static readonly object lockObject = new(); + + public static void SetFactionForTile(PlanetTile tile, Faction faction) + { + lock (lockObject) + { + tileFactions[tile] = faction; + } + } + + public static Faction GetFactionForTile(PlanetTile tile) + { + lock (lockObject) + { + return tileFactions.TryGetValue(tile, out Faction faction) ? faction : null; + } + } + + public static void ClearTile(PlanetTile tile) + { + lock (lockObject) + { + tileFactions.Remove(tile); + } + } + } +} diff --git a/Source/Client/Persistent/CampCreationPatches.cs b/Source/Client/Persistent/CampCreationPatches.cs new file mode 100644 index 00000000..70b3bc9a --- /dev/null +++ b/Source/Client/Persistent/CampCreationPatches.cs @@ -0,0 +1,63 @@ +using HarmonyLib; +using Multiplayer.API; +using Multiplayer.Client.Factions; +using Multiplayer.Client.Util; +using RimWorld; +using RimWorld.Planet; +using System; +using Verse; + +namespace Multiplayer.Client.Persistent +{ + [HarmonyPatch(typeof(SettleInEmptyTileUtility), nameof(SettleInEmptyTileUtility.SetupCamp))] + internal static class SetupCampPatch + { + static void Postfix(Command __result, Caravan caravan) + { + if (Multiplayer.Client == null || __result is not Command_Action cmd) + return; + + cmd.action = () => Sync_Camp(caravan); + } + + [SyncMethod] + private static void Sync_Camp(Caravan caravan) + { + if (caravan == null || caravan.Faction == null) + { + MpLog.Error("[SettleInEmptyTileUtility.SetupCamp] Null caravan or faction in Sync_Camp"); + return; + } + + // Create the camp using the vanilla logic exactly as in SetupCamp + LongEventHandler.QueueLongEvent(delegate + { + TileFactionContext.SetFactionForTile(caravan.Tile, caravan.Faction); + + Map map = GetOrGenerateMapUtility.GetOrGenerateMap(caravan.Tile, Find.World.info.initialMapSize, WorldObjectDefOf.Camp); + + // Set the faction on the camp world object (this is what vanilla does) + map.Parent.SetFaction(caravan.Faction); + + // Enter the caravan into the map + Pawn pawn = caravan.PawnsListForReading[0]; + CaravanEnterMapUtility.Enter(caravan, map, CaravanEnterMode.Center, CaravanDropInventoryMode.DoNotDrop, draftColonists: false, (IntVec3 x) => x.GetRoom(map).CellCount >= 600); + map.Parent.GetComponent()?.StartDetectionCountdown(240000, 60000); + CameraJumper.TryJump(pawn); + + }, "GeneratingMap", doAsynchronously: true, GameAndMapInitExceptionHandlers.ErrorWhileGeneratingMap); + } + } + + [HarmonyPatch(typeof(Current), nameof(Current.Game), MethodType.Setter)] + internal static class CurrentGameSetterPatch + { + static void Prefix(Game value) + { + if (value?.CurrentMap != null) + { + MpLog.Debug($"[CurrentGameSetter] Setting CurrentMap to: {value.CurrentMap.uniqueID} ({value.CurrentMap.Parent?.GetType().Name}), stack trace: {Environment.StackTrace}"); + } + } + } +} diff --git a/Source/Client/Persistent/CaravanFormingPatches.cs b/Source/Client/Persistent/CaravanFormingPatches.cs index bc619635..ffdee910 100644 --- a/Source/Client/Persistent/CaravanFormingPatches.cs +++ b/Source/Client/Persistent/CaravanFormingPatches.cs @@ -11,7 +11,7 @@ namespace Multiplayer.Client.Persistent { - [HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonText), new[] { typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor) })] + [HarmonyPatch(typeof(Widgets), nameof(ButtonText), [typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor)])] static class MakeCancelFormingButtonRed { static void Prefix(string label, ref bool __state) @@ -36,7 +36,7 @@ static void Postfix(bool __state, ref bool __result) } } - [HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))] + [HarmonyPatch(typeof(Widgets), nameof(ButtonTextWorker))] static class FormCaravanHandleReset { static void Prefix(string label, ref bool __state) @@ -185,33 +185,33 @@ static void Prefix(Dialog_FormCaravan __instance, Map map, bool reform, Action o } [SyncMethod] - internal static void StartFormingCaravan(Faction faction, Map map, bool reform = false, IntVec3? designatedMeetingPoint = null, int? routePlannerWaypoint = null) + internal static void StartFormingCaravan(Faction faction, Map map, bool reform = false, IntVec3? designatedMeetingPoint = null, PlanetTile? routePlannerWaypoint = null) { var comp = map.MpComp(); var session = comp.CreateCaravanFormingSession(faction, reform, null, false, designatedMeetingPoint); if (TickPatch.currentExecutingCmdIssuedBySelf) { - var dialog = session.OpenWindow(); - if (routePlannerWaypoint is { } tile) + CaravanFormingProxy dialog = session.OpenWindow(); + if (!routePlannerWaypoint.HasValue) + return; + + try + { + UniqueIdsPatch.useLocalIdsOverride = true; + + // Just to be safe + // RNG shouldn't be invoked but TryAddWaypoint is quite complex and calls pathfinding + Rand.PushState(); + + WorldRoutePlanner worldRoutePlanner = Find.WorldRoutePlanner; + worldRoutePlanner.Start(dialog); + worldRoutePlanner.TryAddWaypoint(routePlannerWaypoint.Value); + } + finally { - try - { - UniqueIdsPatch.useLocalIdsOverride = true; - - // Just to be safe - // RNG shouldn't be invoked but TryAddWaypoint is quite complex and calls pathfinding - Rand.PushState(); - - var worldRoutePlanner = Find.WorldRoutePlanner; - worldRoutePlanner.Start(dialog); - worldRoutePlanner.TryAddWaypoint(tile); - } - finally - { - Rand.PopState(); - UniqueIdsPatch.useLocalIdsOverride = false; - } + Rand.PopState(); + UniqueIdsPatch.useLocalIdsOverride = false; } } } diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index a0a4b69e..45b6622d 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -16,8 +16,8 @@ public class CaravanFormingSession : ExposableSession, ISessionWithTransferables public bool reform; public Action onClosed; public bool mapAboutToBeRemoved; - public int startingTile = -1; - public int destinationTile = -1; + public PlanetTile startingTile = -1; + public PlanetTile destinationTile = -1; public List transferables; public bool autoSelectTravelSupplies; public IntVec3? meetingSpot; @@ -98,7 +98,7 @@ private CaravanFormingProxy PrepareDummyDialog() } [SyncMethod] - public void ChooseRoute(int destination) + public void ChooseRoute(PlanetTile destination) { var dialog = PrepareDummyDialog(); dialog.Notify_ChoseRoute(destination); From f776396a49be759408a09d13bd2cede76347cc68 Mon Sep 17 00:00:00 2001 From: Reznal Date: Sun, 13 Jul 2025 23:37:32 +1000 Subject: [PATCH 32/38] Fixed an issue with player counting for maps --- Source/Client/Patches/VTRSyncPatch.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index b6a522e3..40704525 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -79,7 +79,7 @@ static void Prefix(Map value) return; // Prevent duplicate commands for the same transition, but allow retry after a tick - if (VTRSync.lastMovedToMap == previousMap && currentTick == VTRSync.lastSentTick) + if (VTRSync.lastMovedToMap == newMap && currentTick == VTRSync.lastSentTick) return; // Send map change command to server @@ -116,12 +116,6 @@ static void Postfix(WorldRenderMode __result) Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.lastMovedToMap, VTRSync.WorldMapId)); } } - // Detect transition away from world map - else if (__result != WorldRenderMode.Planet && lastRenderMode == WorldRenderMode.Planet) - { - int currentMapId = Find.CurrentMap?.uniqueID ?? -1; - Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.WorldMapId, currentMapId)); - } lastRenderMode = __result; } From 77984789325b9392556b217d17e3532dbe293d53 Mon Sep 17 00:00:00 2001 From: Reznal Date: Mon, 14 Jul 2025 19:07:52 +1000 Subject: [PATCH 33/38] Added a new fix for rbp to potentially fix this running on different systems --- Source/Common/DeferredStackTracingImpl.cs | 46 ++++++++++++----------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index fa3f9317..38c93582 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -37,18 +37,7 @@ struct AddrInfo public const int HashInfluence = 6; private static unsafe delegate* getRbpFunc; - - static unsafe DeferredStackTracingImpl() - { - getRbpFunc = Application.platform switch - { - RuntimePlatform.LinuxEditor => &GetRbpWindows, - RuntimePlatform.LinuxPlayer => &GetRbpWindows, - RuntimePlatform.OSXEditor => &GetRbpMac, - RuntimePlatform.OSXPlayer => &GetRbpMac, - _ => &GetRbpWindows - }; - } + static unsafe DeferredStackTracingImpl() => getRbpFunc = &GetRbpProbed; public static unsafe int TraceImpl(long[] traceIn, ref int hash) { @@ -253,18 +242,31 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage) } } - [MethodImpl(MethodImplOptions.NoInlining)] - static unsafe long GetRbpMac() + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static unsafe long GetRbpProbed() { - long rbp = 0; - return *(&rbp + 1); // Mac offset - } + long local = 0; + long* p = &local; - [MethodImpl(MethodImplOptions.NoInlining)] - static unsafe long GetRbpWindows() - { - long rbp = 0; - return *(&rbp + 4); // Windows offset + // Scan up to 32 q-words (~256 B) – copes with big frames from extra locals, + // tier-1 JIT prologues and the 6-register spill variant. + for (int i = 1; i <= 32; i++) + { + long cand = *(&local + i); + + if (cand <= (long)p) continue; // must be above us + if ((cand & 7) != 0) continue; // 8-byte aligned + + // Cheap sanity: return address slot should be executable + if (Native.mono_jit_info_table_find(Native.DomainPtr, *(IntPtr*)(cand + 8)) == IntPtr.Zero) + continue; + + Verse.Log.Message($"[rbp] saved [{i}] RBP 0x{*(&local + i):X}"); + return cand; // looks like a real frame ptr + } + + // Fallback – old +1 assumption + return *(p + 1); } public static int HashCombineInt(int seed, int value) From 90bfd7eaaef70ce71277a5d53144a7c0c0bd1149 Mon Sep 17 00:00:00 2001 From: Reznal Date: Mon, 14 Jul 2025 20:12:57 +1000 Subject: [PATCH 34/38] Removed obnoxious log --- Source/Common/DeferredStackTracingImpl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Common/DeferredStackTracingImpl.cs b/Source/Common/DeferredStackTracingImpl.cs index 38c93582..149c689e 100644 --- a/Source/Common/DeferredStackTracingImpl.cs +++ b/Source/Common/DeferredStackTracingImpl.cs @@ -261,7 +261,6 @@ private static unsafe long GetRbpProbed() if (Native.mono_jit_info_table_find(Native.DomainPtr, *(IntPtr*)(cand + 8)) == IntPtr.Zero) continue; - Verse.Log.Message($"[rbp] saved [{i}] RBP 0x{*(&local + i):X}"); return cand; // looks like a real frame ptr } From 2f1d2d4dcc562f902ebca14560500d92ac4946c8 Mon Sep 17 00:00:00 2001 From: Dillion Lowry Date: Mon, 14 Jul 2025 14:33:31 -0500 Subject: [PATCH 35/38] Add PerformanceRecorder class, update debug panel --- Source/Client/OnMainThread.cs | 6 + Source/Client/UI/DebugPanel/DebugLine.cs | 24 +- Source/Client/UI/DebugPanel/DebugSection.cs | 17 +- .../UI/DebugPanel/PerformanceRecorder.cs | 436 ++++++++++++++++++ Source/Client/UI/DebugPanel/StatusBadge.cs | 45 +- Source/Client/UI/DebugPanel/SyncDebugPanel.cs | 73 ++- 6 files changed, 573 insertions(+), 28 deletions(-) create mode 100644 Source/Client/UI/DebugPanel/PerformanceRecorder.cs diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index b07999b6..f6d0e099 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -1,3 +1,4 @@ +using Multiplayer.Client.DebugUi; using Multiplayer.Client.Networking; using Multiplayer.Common; using System; @@ -42,6 +43,11 @@ public void Update() Multiplayer.session.Update(); SyncFieldUtil.UpdateSync(); + if (PerformanceRecorder.IsRecording) + { + PerformanceRecorder.RecordFrame(); + } + if (!Multiplayer.arbiterInstance && Application.isFocused && !TickPatch.Simulating && !Multiplayer.session.desynced) Multiplayer.session.playerCursors.SendVisuals(); diff --git a/Source/Client/UI/DebugPanel/DebugLine.cs b/Source/Client/UI/DebugPanel/DebugLine.cs index ac410fd4..ff5de421 100644 --- a/Source/Client/UI/DebugPanel/DebugLine.cs +++ b/Source/Client/UI/DebugPanel/DebugLine.cs @@ -2,21 +2,19 @@ namespace Multiplayer.Client.DebugUi { -public static partial class SyncDebugPanel + // Data structures for organizing debug sections + internal struct DebugLine { - // Data structures for organizing debug sections - private struct DebugLine - { - public string Label; - public string Value; - public Color Color; + public string Label; + public string Value; + public Color Color; - public DebugLine(string label, string value, Color color) - { - Label = label; - Value = value; - Color = color; - } + public DebugLine(string label, string value, Color color) + { + Label = label; + Value = value; + Color = color; } } + } diff --git a/Source/Client/UI/DebugPanel/DebugSection.cs b/Source/Client/UI/DebugPanel/DebugSection.cs index 659fe36e..119390c3 100644 --- a/Source/Client/UI/DebugPanel/DebugSection.cs +++ b/Source/Client/UI/DebugPanel/DebugSection.cs @@ -1,17 +1,14 @@ namespace Multiplayer.Client.DebugUi { -public static partial class SyncDebugPanel + internal struct DebugSection { - private struct DebugSection - { - public string Title; - public DebugLine[] Lines; + public string Title; + public DebugLine[] Lines; - public DebugSection(string title, DebugLine[] lines) - { - Title = title; - Lines = lines; - } + public DebugSection(string title, DebugLine[] lines) + { + Title = title; + Lines = lines; } } } diff --git a/Source/Client/UI/DebugPanel/PerformanceRecorder.cs b/Source/Client/UI/DebugPanel/PerformanceRecorder.cs new file mode 100644 index 00000000..8338a139 --- /dev/null +++ b/Source/Client/UI/DebugPanel/PerformanceRecorder.cs @@ -0,0 +1,436 @@ +using Multiplayer.Client.Patches; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.DebugUi; + +/// +/// Records performance and networking diagnostics during multiplayer sessions. +/// Tracks averages, highs, and lows for various metrics and outputs results to file and console. +/// +public static class PerformanceRecorder +{ + private static bool isRecording = false; + private static DateTime recordingStartTime; + private static int recordingFrameCount = 0; + + // Performance metrics + private static List frameTimeSamples = new List(); + private static List tickTimeSamples = new List(); + private static List deltaTimeSamples = new List(); + private static List fpsSamples = new List(); + private static List tpsSamples = new List(); + private static List normalizedTpsSamples = new List(); + private static List serverTPTSamples = new List(); + + // Networking metrics + private static List timerLagSamples = new List(); + private static List receivedCmdsSamples = new List(); + private static List sentCmdsSamples = new List(); + private static List bufferedChangesSamples = new List(); + private static List mapCmdsSamples = new List(); + private static List worldCmdsSamples = new List(); + + // Memory/GC metrics + private static List clientOpinionsSamples = new List(); + private static List worldPawnsSamples = new List(); + private static List windowCountSamples = new List(); + + public static bool IsRecording => isRecording; + public static int FrameCount => recordingFrameCount; + public static TimeSpan RecordingDuration => isRecording ? DateTime.Now - recordingStartTime : TimeSpan.Zero; + public static float AverageFPS => fpsSamples.Count > 0 ? fpsSamples.Average() : 0f; + public static float AverageTPS => tpsSamples.Count > 0 ? tpsSamples.Average() : 0f; + public static float AverageNormalizedTPS => normalizedTpsSamples.Count > 0 ? normalizedTpsSamples.Average() : 0f; + + /// + /// Get recording status for debug panel display + /// + public static StatusBadge GetRecordingStatus() + { + if (!isRecording) + return new StatusBadge("⏸", Color.gray, "REC", "Performance recording is stopped"); + + var duration = DateTime.Now - recordingStartTime; + return new StatusBadge("⏺", Color.red, $"REC {duration.TotalSeconds:F0}s", $"Recording performance for {duration.TotalSeconds:F1} seconds"); + } + + public static void StartRecording() + { + if (isRecording) return; + + isRecording = true; + recordingStartTime = DateTime.Now; + recordingFrameCount = 0; + + // Clear previous samples + frameTimeSamples.Clear(); + tickTimeSamples.Clear(); + deltaTimeSamples.Clear(); + fpsSamples.Clear(); + tpsSamples.Clear(); + normalizedTpsSamples.Clear(); + serverTPTSamples.Clear(); + timerLagSamples.Clear(); + receivedCmdsSamples.Clear(); + sentCmdsSamples.Clear(); + bufferedChangesSamples.Clear(); + mapCmdsSamples.Clear(); + worldCmdsSamples.Clear(); + clientOpinionsSamples.Clear(); + worldPawnsSamples.Clear(); + windowCountSamples.Clear(); + + Verse.Log.Message("[PerformanceRecorder] Recording started"); + } + + public static void StopRecording() + { + if (!isRecording) return; + + isRecording = false; + var recordingDuration = DateTime.Now - recordingStartTime; + + Verse.Log.Message("[PerformanceRecorder] Recording stopped"); + + // Generate and output results + var results = GenerateResults(recordingDuration); + OutputToConsole(results); + OutputToFile(results); + } + + public static void RecordFrame() + { + if (!isRecording) return; + + recordingFrameCount++; + + // Performance metrics + frameTimeSamples.Add(Time.deltaTime * 1000f); + tickTimeSamples.Add(TickPatch.tickTimer?.ElapsedMilliseconds ?? 0); + deltaTimeSamples.Add(Time.deltaTime * 60f); + fpsSamples.Add(1f / Time.deltaTime); + + // Networking metrics + if (Multiplayer.Client != null) + { + timerLagSamples.Add(TickPatch.tickUntil - TickPatch.Timer); + receivedCmdsSamples.Add(Multiplayer.session?.receivedCmds ?? 0); + sentCmdsSamples.Add(Multiplayer.session?.remoteSentCmds ?? 0); + bufferedChangesSamples.Add(SyncFieldUtil.bufferedChanges?.Sum(kv => kv.Value?.Count ?? 0) ?? 0); + + if (Find.CurrentMap?.AsyncTime() != null) + { + var async = Find.CurrentMap.AsyncTime(); + float currentTps = IngameUIPatch.tps; + tpsSamples.Add(currentTps); + normalizedTpsSamples.Add(StatusBadge.GetNormalizedTPS(currentTps)); + serverTPTSamples.Add(TickPatch.serverTimePerTick); + mapCmdsSamples.Add(async.cmds?.Count ?? 0); + worldCmdsSamples.Add(Multiplayer.AsyncWorldTime?.cmds?.Count ?? 0); + } + + clientOpinionsSamples.Add(Multiplayer.game?.sync?.knownClientOpinions?.Count ?? 0); + } + + // Memory/system metrics + worldPawnsSamples.Add(Find.WorldPawns?.AllPawnsAliveOrDead?.Count ?? 0); + windowCountSamples.Add(Find.WindowStack?.windows?.Count ?? 0); + } + + private static PerformanceResults GenerateResults(TimeSpan duration) + { + return new PerformanceResults + { + Duration = duration, + FrameCount = recordingFrameCount, + StartTime = recordingStartTime, + + // Performance stats + FrameTime = CalculateStats(frameTimeSamples), + TickTime = CalculateStats(tickTimeSamples), + DeltaTime = CalculateStats(deltaTimeSamples), + FPS = CalculateStats(fpsSamples), + TPS = CalculateStats(tpsSamples), + NormalizedTPS = CalculateStats(normalizedTpsSamples), + ServerTPT = CalculateStats(serverTPTSamples), + + // Networking stats + TimerLag = CalculateStats(timerLagSamples.Select(x => (float)x)), + ReceivedCmds = CalculateStats(receivedCmdsSamples.Select(x => (float)x)), + SentCmds = CalculateStats(sentCmdsSamples.Select(x => (float)x)), + BufferedChanges = CalculateStats(bufferedChangesSamples.Select(x => (float)x)), + MapCmds = CalculateStats(mapCmdsSamples.Select(x => (float)x)), + WorldCmds = CalculateStats(worldCmdsSamples.Select(x => (float)x)), + + // Memory stats + ClientOpinions = CalculateStats(clientOpinionsSamples.Select(x => (float)x)), + WorldPawns = CalculateStats(worldPawnsSamples.Select(x => (float)x)), + WindowCount = CalculateStats(windowCountSamples.Select(x => (float)x)) + }; + } + + private static StatResult CalculateStats(IEnumerable samples) + { + var list = samples.ToList(); + if (list.Count == 0) + return new StatResult { Average = 0, Min = 0, Max = 0, Count = 0 }; + + return new StatResult + { + Average = list.Average(), + Min = list.Min(), + Max = list.Max(), + Count = list.Count + }; + } + + private static void OutputToConsole(PerformanceResults results) + { + var sb = new StringBuilder(); + sb.AppendLine("-- MULTIPLAYER PERFORMANCE RECORDING RESULTS --"); + sb.AppendLine($"Recording Duration: {results.Duration.TotalSeconds:F2} seconds"); + sb.AppendLine($"Total Frames: {results.FrameCount:N0}"); + sb.AppendLine($"Recording Start: {results.StartTime:yyyy-MM-dd HH:mm:ss}"); + sb.AppendLine(); + + sb.AppendLine("PERFORMANCE METRICS:"); + sb.AppendLine($" Frame Time: Avg {results.FrameTime.Average:F2}ms Min {results.FrameTime.Min:F2}ms Max {results.FrameTime.Max:F2}ms"); + sb.AppendLine($" Tick Time: Avg {results.TickTime.Average:F2}ms Min {results.TickTime.Min:F2}ms Max {results.TickTime.Max:F2}ms"); + sb.AppendLine($" FPS: Avg {results.FPS.Average:F1} Min {results.FPS.Min:F1} Max {results.FPS.Max:F1}"); + sb.AppendLine($" TPS (Raw): Avg {results.TPS.Average:F2} Min {results.TPS.Min:F2} Max {results.TPS.Max:F2}"); + sb.AppendLine($" TPS (Norm %): Avg {results.NormalizedTPS.Average:F1}% Min {results.NormalizedTPS.Min:F1}% Max {results.NormalizedTPS.Max:F1}%"); + sb.AppendLine($" Server TPT: Avg {results.ServerTPT.Average:F2}ms Min {results.ServerTPT.Min:F2}ms Max {results.ServerTPT.Max:F2}ms"); + sb.AppendLine(); + + sb.AppendLine("NETWORKING METRICS:"); + sb.AppendLine($" Timer Lag: Avg {results.TimerLag.Average:F1} Min {results.TimerLag.Min:F0} Max {results.TimerLag.Max:F0}"); + sb.AppendLine($" Received Cmds: Avg {results.ReceivedCmds.Average:F0} Min {results.ReceivedCmds.Min:F0} Max {results.ReceivedCmds.Max:F0}"); + sb.AppendLine($" Sent Cmds: Avg {results.SentCmds.Average:F0} Min {results.SentCmds.Min:F0} Max {results.SentCmds.Max:F0}"); + sb.AppendLine($" Buffered Changes: Avg {results.BufferedChanges.Average:F0} Min {results.BufferedChanges.Min:F0} Max {results.BufferedChanges.Max:F0}"); + sb.AppendLine($" Map Commands: Avg {results.MapCmds.Average:F0} Min {results.MapCmds.Min:F0} Max {results.MapCmds.Max:F0}"); + sb.AppendLine($" World Commands: Avg {results.WorldCmds.Average:F0} Min {results.WorldCmds.Min:F0} Max {results.WorldCmds.Max:F0}"); + sb.AppendLine(); + + sb.AppendLine("SYSTEM METRICS:"); + sb.AppendLine($" Client Opinions: Avg {results.ClientOpinions.Average:F0} Min {results.ClientOpinions.Min:F0} Max {results.ClientOpinions.Max:F0}"); + sb.AppendLine($" World Pawns: Avg {results.WorldPawns.Average:F0} Min {results.WorldPawns.Min:F0} Max {results.WorldPawns.Max:F0}"); + sb.AppendLine($" Window Count: Avg {results.WindowCount.Average:F0} Min {results.WindowCount.Min:F0} Max {results.WindowCount.Max:F0}"); + sb.AppendLine(); + sb.AppendLine("-- END RECORDING RESULTS --"); + + Verse.Log.Message(sb.ToString()); + } + + private static void OutputToFile(PerformanceResults results) + { + try + { + var fileName = $"mp_performance_{results.StartTime:yyyyMMdd_HHmmss}.txt"; + var filePath = Path.Combine(GenFilePaths.SaveDataFolderPath, fileName); + + var sb = new StringBuilder(); + sb.AppendLine("MULTIPLAYER PERFORMANCE RECORDING RESULTS"); + sb.AppendLine("-----------------------------------------"); + sb.AppendLine($"Recording Start: {results.StartTime:yyyy-MM-dd HH:mm:ss}"); + sb.AppendLine($"Recording Duration: {results.Duration.TotalSeconds:F2} seconds"); + sb.AppendLine($"Total Frames Recorded: {results.FrameCount:N0}"); + sb.AppendLine($"Average Frame Rate: {results.FrameCount / results.Duration.TotalSeconds:F1} FPS"); + sb.AppendLine(); + + sb.AppendLine("PERFORMANCE METRICS"); + sb.AppendLine("-------------------"); + AppendDetailedStats(sb, "Frame Time (ms)", results.FrameTime); + AppendDetailedStats(sb, "Tick Time (ms)", results.TickTime); + AppendDetailedStats(sb, "Delta Time", results.DeltaTime); + AppendDetailedStats(sb, "Frames Per Second", results.FPS); + AppendDetailedStats(sb, "Ticks Per Second (Raw)", results.TPS); + AppendDetailedStats(sb, "TPS Performance (%)", results.NormalizedTPS); + AppendDetailedStats(sb, "Server Time Per Tick (ms)", results.ServerTPT); + sb.AppendLine(); + + sb.AppendLine("NETWORKING METRICS"); + sb.AppendLine("------------------"); + AppendDetailedStats(sb, "Timer Lag", results.TimerLag); + AppendDetailedStats(sb, "Received Commands", results.ReceivedCmds); + AppendDetailedStats(sb, "Sent Commands", results.SentCmds); + AppendDetailedStats(sb, "Buffered Changes", results.BufferedChanges); + AppendDetailedStats(sb, "Map Commands", results.MapCmds); + AppendDetailedStats(sb, "World Commands", results.WorldCmds); + sb.AppendLine(); + + sb.AppendLine("SYSTEM METRICS"); + sb.AppendLine("--------------"); + AppendDetailedStats(sb, "Client Opinions", results.ClientOpinions); + AppendDetailedStats(sb, "World Pawns", results.WorldPawns); + AppendDetailedStats(sb, "Window Count", results.WindowCount); + sb.AppendLine(); + + sb.AppendLine("SYSTEM INFORMATION"); + sb.AppendLine("------------------"); + sb.AppendLine($"RimWorld Version: {VersionControl.CurrentVersionStringWithRev}"); + sb.AppendLine($"Multiplayer Version: {MpVersion.Version}"); + sb.AppendLine($"Unity Version: {Application.unityVersion}"); + sb.AppendLine($"Platform: {Application.platform}"); + sb.AppendLine($"System Memory: {SystemInfo.systemMemorySize} MB"); + sb.AppendLine($"Graphics Memory: {SystemInfo.graphicsMemorySize} MB"); + sb.AppendLine($"Processor: {SystemInfo.processorType} ({SystemInfo.processorCount} cores)"); + + File.WriteAllText(filePath, sb.ToString()); + Verse.Log.Message($"[PerformanceRecorder] Results saved to: {filePath}"); + } + catch (Exception ex) + { + Verse.Log.Error($"[PerformanceRecorder] Failed to save results to file: {ex.Message}"); + } + } + + private static void AppendDetailedStats(StringBuilder sb, string name, StatResult stats) + { + sb.AppendLine($"{name,-25} Avg: {stats.Average,8:F2} Min: {stats.Min,8:F2} Max: {stats.Max,8:F2} Samples: {stats.Count,6:N0}"); + } + + /// + /// Draw performance recorder section for debug panel + /// + public static float DrawPerformanceRecorderSection(float x, float y, float width) + { + StatusBadge recordingStatus = GetRecordingStatus(); + + var lines = new List + { + new DebugLine("Status:", recordingStatus.text, recordingStatus.color), + new DebugLine("Frame Count:", recordingFrameCount.ToString(), Color.white) + }; + + if (isRecording) + { + var duration = DateTime.Now - recordingStartTime; + lines.Add(new DebugLine("Duration:", $"{duration.TotalSeconds:F1}s", Color.white)); + lines.Add(new DebugLine("Avg FPS:", frameTimeSamples.Count > 0 ? $"{fpsSamples.Average():F1}" : "N/A", Color.white)); + lines.Add(new DebugLine("Avg TPS:", tpsSamples.Count > 0 ? $"{tpsSamples.Average():F1}" : "N/A", Color.white)); + } + + var section = new DebugSection("PERFORMANCE RECORDER", lines.ToArray()); + return DrawSection(x, y, width, section); + } + + /// + /// Draw performance recorder controls for debug panel + /// + public static float DrawPerformanceRecorderControls(float x, float y, float width) + { + var buttonWidth = 60f; + var buttonHeight = 20f; + var spacing = 4f; + + var startRect = new Rect(x, y, buttonWidth, buttonHeight); + var stopRect = new Rect(x + buttonWidth + spacing, y, buttonWidth, buttonHeight); + + GUI.color = isRecording ? Color.gray : Color.white; + + if (Widgets.ButtonText(startRect, "Start") && !isRecording) + { + StartRecording(); + } + + + GUI.color = !isRecording ? Color.gray : Color.white; + + if (Widgets.ButtonText(stopRect, "Stop") && isRecording) + { + StopRecording(); + } + + + UnityEngine.GUI.color = Color.white; + return buttonHeight + spacing; + } + + // Helper method to draw sections (copied from SyncDebugPanel pattern) + private static float DrawSection(float x, float y, float width, DebugSection section) + { + float startY = y; + const float LineHeight = 18f; + const float SectionSpacing = 10f; + const float LabelColumnWidth = 0.5f; + + // Draw section header + var headerRect = new Rect(x, y, width, LineHeight + 2f); + using (MpStyle.Set(GameFont.Small).Set(Color.cyan).Set(TextAnchor.MiddleLeft)) + { + Widgets.Label(headerRect, $"── {section.Title} ──"); + } + y += LineHeight + 6f; + + // Draw section lines + foreach (var line in section.Lines) + { + var labelWidth = width * LabelColumnWidth; + var valueWidth = width * (1f - LabelColumnWidth); + + // Label + using (MpStyle.Set(GameFont.Tiny).Set(Color.white).Set(TextAnchor.MiddleLeft)) + { + var labelRect = new Rect(x, y, labelWidth - 4f, LineHeight); + Widgets.Label(labelRect, line.Label); + } + + // Value + using (MpStyle.Set(GameFont.Tiny).Set(line.Color).Set(TextAnchor.MiddleLeft)) + { + var valueRect = new Rect(x + labelWidth + 4f, y, valueWidth - 4f, LineHeight); + Widgets.Label(valueRect, line.Value); + } + + y += LineHeight + 1f; + } + + return y - startY + SectionSpacing; + } +} + +public class PerformanceResults +{ + public TimeSpan Duration; + public int FrameCount; + public DateTime StartTime; + + // Performance + public StatResult FrameTime; + public StatResult TickTime; + public StatResult DeltaTime; + public StatResult FPS; + public StatResult TPS; + public StatResult NormalizedTPS; + public StatResult ServerTPT; + + // Networking + public StatResult TimerLag; + public StatResult ReceivedCmds; + public StatResult SentCmds; + public StatResult BufferedChanges; + public StatResult MapCmds; + public StatResult WorldCmds; + + // System + public StatResult ClientOpinions; + public StatResult WorldPawns; + public StatResult WindowCount; +} + +public struct StatResult +{ + public float Average; + public float Min; + public float Max; + public int Count; +} diff --git a/Source/Client/UI/DebugPanel/StatusBadge.cs b/Source/Client/UI/DebugPanel/StatusBadge.cs index 9c382b49..309aae79 100644 --- a/Source/Client/UI/DebugPanel/StatusBadge.cs +++ b/Source/Client/UI/DebugPanel/StatusBadge.cs @@ -1,4 +1,5 @@ using Multiplayer.Client.Patches; +using System; using UnityEngine; using Verse; @@ -33,9 +34,49 @@ public static StatusBadge GetSyncStatus() public static StatusBadge GetPerformanceStatus() { float tps = IngameUIPatch.tps; - string tooltip = tps > 40f ? "Performance is good" : tps > 20f ? "Performance is moderate" : "Performance is poor"; + float normalizedTps = GetNormalizedTPS(tps); + + string tooltip = normalizedTps >= 90f ? "Performance is excellent" : + normalizedTps >= 70f ? "Performance is good" : + normalizedTps >= 50f ? "Performance is moderate" : + normalizedTps >= 25f ? "Performance is poor" : + "Performance is very poor"; - return new StatusBadge("▲", GetPerformanceColor(tps, 40f, 20f), $"{tps:F1}", tooltip); + return new StatusBadge("▲", GetPerformanceColor(normalizedTps, 90f, 70f), $"{normalizedTps:F0}%", tooltip); + } + + /// + /// Get the target TPS for the current game speed + /// + public static float GetTargetTPS() + { + if (Find.TickManager == null) return 60f; + if (Find.TickManager.Paused) return 0f; + + return Find.TickManager.CurTimeSpeed switch + { + TimeSpeed.Paused => 0f, + TimeSpeed.Normal => 60f, + TimeSpeed.Fast => 180f, + TimeSpeed.Superfast => 360f, + TimeSpeed.Ultrafast => 900f, // Debug mode + _ => 60f + }; + } + + /// + /// Get normalized TPS performance as percentage (0-100%) + /// + public static float GetNormalizedTPS(float currentTps) + { + float targetTps = GetTargetTPS(); + + // If paused, return 100% + if (targetTps == 0f) + return 100f; + + float percentage = (currentTps / targetTps) * 100f; + return Math.Min(percentage, 100f); } public static StatusBadge GetTickStatus() diff --git a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs index 2587a7af..1372d9ed 100644 --- a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs +++ b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs @@ -24,7 +24,7 @@ public static partial class SyncDebugPanel // Panel dimensions private const float HeaderHeight = 40f; private const float VisibleExpandedHeight = 400f; - private const float ContentHeight = 1300f; + private const float ContentHeight = 1500f; private const float PanelWidth = 275f; // Visual constants @@ -184,6 +184,7 @@ private static void DrawExpandedPanel(Rect rect) currentY += DrawStatusSummarySection(viewRect.x + Margin, currentY, viewRect.width - Margin); currentY += DrawRngStatesSection(viewRect.x + Margin, currentY, viewRect.width - Margin); currentY += DrawPerformanceSection(viewRect.x + Margin, currentY, viewRect.width - Margin); + currentY += DrawPerformanceRecorderSection(viewRect.x + Margin, currentY, viewRect.width - Margin); currentY += DrawNetworkSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin); currentY += DrawCoreSystemSection(viewRect.x + Margin, currentY, viewRect.width - Margin); currentY += DrawTimingSyncSection(viewRect.x + Margin, currentY, viewRect.width - Margin); @@ -234,6 +235,68 @@ private static float DrawStatusSummarySection(float x, float y, float width) return DrawSection(x, y, width, section); } + /// + /// Draw performance recorder section with controls + /// + private static float DrawPerformanceRecorderSection(float x, float y, float width) + { + StatusBadge recordingStatus = PerformanceRecorder.GetRecordingStatus(); + + var lines = new List + { + new("Status:", recordingStatus.text, recordingStatus.color), + new("Frame Count:", PerformanceRecorder.FrameCount.ToString(), Color.white) + }; + + if (PerformanceRecorder.IsRecording) + { + var duration = PerformanceRecorder.RecordingDuration; + lines.Add(new("Duration:", $"{duration.TotalSeconds:F1}s", Color.white)); + lines.Add(new("Avg FPS:", PerformanceRecorder.AverageFPS > 0 ? $"{PerformanceRecorder.AverageFPS:F1}" : "N/A", Color.white)); + lines.Add(new("Avg TPS:", PerformanceRecorder.AverageTPS > 0 ? $"{PerformanceRecorder.AverageTPS:F1}" : "N/A", Color.white)); + lines.Add(new("TPS Perf:", PerformanceRecorder.AverageNormalizedTPS > 0 ? $"{PerformanceRecorder.AverageNormalizedTPS:F1}%" : "N/A", Color.white)); + } + + var section = new DebugSection("PERFORMANCE RECORDER", lines.ToArray()); + float sectionHeight = DrawSection(x, y, width, section); + + // Add control buttons below the section + float buttonY = y + sectionHeight - SectionSpacing + 4f; + float buttonHeight = DrawRecorderControls(x, buttonY, width); + + return sectionHeight + buttonHeight; + } + + /// + /// Draw performance recorder control buttons + /// + private static float DrawRecorderControls(float x, float y, float width) + { + var buttonWidth = 60f; + var buttonHeight = 20f; + var spacing = 4f; + + var startRect = new Rect(x, y, buttonWidth, buttonHeight); + var stopRect = new Rect(x + buttonWidth + spacing, y, buttonWidth, buttonHeight); + + GUI.color = PerformanceRecorder.IsRecording ? Color.gray : Color.white; + + if (Widgets.ButtonText(startRect, "Start") && !PerformanceRecorder.IsRecording) + { + PerformanceRecorder.StartRecording(); + } + + GUI.color = !PerformanceRecorder.IsRecording ? Color.gray : Color.white; + + if (Widgets.ButtonText(stopRect, "Stop") && PerformanceRecorder.IsRecording) + { + PerformanceRecorder.StopRecording(); + } + + GUI.color = Color.white; + return buttonHeight + spacing; + } + /// /// Draw RNG states comparison section /// @@ -276,14 +339,18 @@ private static float DrawPerformanceSection(float x, float y, float width) { // TPS float tps = IngameUIPatch.tps; - Color tpsColor = StatusBadge.GetPerformanceColor(tps, 40f, 20f); + float normalizedTps = StatusBadge.GetNormalizedTPS(tps); + float targetTps = StatusBadge.GetTargetTPS(); + Color tpsColor = StatusBadge.GetPerformanceColor(normalizedTps, 90f, 70f); // Frame time float frameTime = Time.deltaTime * 1000f; Color frameColor = StatusBadge.GetPerformanceColor(frameTime, 20f, 35f, true); DebugLine[] lines = [ - new("Map TPS:", $"{tps:F1}", tpsColor), + new("Map TPS:", $"{tps:F1}", Color.white), + new("Target TPS:", $"{targetTps:F0}", Color.gray), + new("TPS Perf:", $"{normalizedTps:F1}%", tpsColor), new("Frame Time:", $"{frameTime:F1}ms", frameColor), new("Server TPT:", $"{TickPatch.serverTimePerTick:F1}ms", Color.white), new("Avg Frame:", $"{TickPatch.avgFrameTime:F1}ms", Color.white) From f0f2f698d042260367e2354324388ea2b8b80ea7 Mon Sep 17 00:00:00 2001 From: Dillion Lowry Date: Mon, 14 Jul 2025 20:26:09 -0500 Subject: [PATCH 36/38] Add PerformancCalculator for TPS calculations, update recorder and debug panel --- .../UI/DebugPanel/PerformanceCalculator.cs | 191 ++++++++++++++++++ .../UI/DebugPanel/PerformanceRecorder.cs | 45 +++-- Source/Client/UI/DebugPanel/StatusBadge.cs | 55 +---- Source/Client/UI/DebugPanel/SyncDebugPanel.cs | 34 +++- 4 files changed, 257 insertions(+), 68 deletions(-) create mode 100644 Source/Client/UI/DebugPanel/PerformanceCalculator.cs diff --git a/Source/Client/UI/DebugPanel/PerformanceCalculator.cs b/Source/Client/UI/DebugPanel/PerformanceCalculator.cs new file mode 100644 index 00000000..0dadd319 --- /dev/null +++ b/Source/Client/UI/DebugPanel/PerformanceCalculator.cs @@ -0,0 +1,191 @@ +using Multiplayer.Client.AsyncTime; +using System; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.DebugUi +{ + /// + /// Calculates performance based on current TPS and target TPS. Handles speed changes and stabilization periods. + /// + public static class PerformanceCalculator + { + private static TimeSpeed lastTimeSpeed = TimeSpeed.Normal; + private static float lastSpeedChangeTime = 0f; + private const float SpeedStabilizationTime = 2.5f; + + // AsyncTime caching for performance + private static Map lastCachedMap = null; + private static AsyncTimeComp cachedAsyncTimeComp = null; + private static float lastCacheTime = 0f; + private const float CacheValidTime = 0.1f; // 100ms + + private static AsyncTimeComp GetCachedAsyncTimeComp() + { + var currentMap = Find.CurrentMap; + var currentTime = Time.realtimeSinceStartup; + + if (currentMap == lastCachedMap && + cachedAsyncTimeComp != null && + currentTime - lastCacheTime < CacheValidTime) + { + return cachedAsyncTimeComp; + } + + lastCachedMap = currentMap; + lastCacheTime = currentTime; + cachedAsyncTimeComp = currentMap?.AsyncTime(); + + return cachedAsyncTimeComp; + } + + /// + /// Get the effective TPS target considering all speed-limiting mechanisms + /// + public static float GetTargetTPS() + { + if (TickPatch.Frozen) return 0f; + + if (Multiplayer.GameComp?.asyncTime == true) + { + // Try map async time first + if (GetCachedAsyncTimeComp() is AsyncTimeComp asyncTimeComp) + { + var desiredSpeed = asyncTimeComp.DesiredTimeSpeed; + float rateMultiplier = asyncTimeComp.TickRateMultiplier(desiredSpeed); + + var currentTicks = asyncTimeComp.mapTicks; + var forceNormalUntil = Find.TickManager?.slower?.forceNormalSpeedUntil ?? 0; + + if (currentTicks < forceNormalUntil) return 60f; + return CalculateTPS(rateMultiplier, desiredSpeed); + } + + // Fallback to world async time + if (Multiplayer.AsyncWorldTime is AsyncWorldTimeComp comp) + { + var worldDesiredSpeed = comp.DesiredTimeSpeed; + float worldRateMultiplier = comp.TickRateMultiplier(worldDesiredSpeed); + return CalculateTPS(worldRateMultiplier, worldDesiredSpeed); + } + } + else + { + // Non-async mode, the actual rate is the minimum across all maps + if (GetCachedAsyncTimeComp() is AsyncTimeComp currentTickable) + { + var desiredSpeed = currentTickable.DesiredTimeSpeed; + float actualRateMultiplier = currentTickable.ActualRateMultiplier(desiredSpeed); + return CalculateTPS(actualRateMultiplier, desiredSpeed); + } + } + + // Fallback to standard TickManager calculation + if (IsForcedNormal() || Find.TickManager is not TickManager tickManager) + { + return 60f; + } + + if (tickManager.Paused || tickManager.ForcePaused) return 0f; + + return tickManager.CurTimeSpeed switch + { + TimeSpeed.Paused => 0f, + TimeSpeed.Normal => 60f, + TimeSpeed.Fast => 180f, + TimeSpeed.Superfast => 360f, + TimeSpeed.Ultrafast => 900f, + _ => 60f + }; + } + + /// + /// Check if we're in a stabilization period after a speed change + /// + public static bool IsInStabilizationPeriod() + { + if (Find.TickManager is not TickManager tickManager) return false; + + TimeSpeed currentSpeed; + if (Multiplayer.GameComp?.asyncTime == true) + { + if (GetCachedAsyncTimeComp() is AsyncTimeComp asyncTimeComp) + { + currentSpeed = asyncTimeComp.DesiredTimeSpeed; + } + else + { + currentSpeed = Multiplayer.AsyncWorldTime?.DesiredTimeSpeed ?? + (tickManager.Paused ? TimeSpeed.Paused : tickManager.CurTimeSpeed); + } + } + else + { + currentSpeed = tickManager.Paused ? TimeSpeed.Paused : tickManager.CurTimeSpeed; + } + + if (currentSpeed != lastTimeSpeed) + { + lastTimeSpeed = currentSpeed; + lastSpeedChangeTime = Time.realtimeSinceStartup; + + // Invalidate AsyncTime cache to pick up changes quickly + cachedAsyncTimeComp = null; + lastCachedMap = null; + + return true; + } + + return Time.realtimeSinceStartup - lastSpeedChangeTime < SpeedStabilizationTime; + } + + /// + /// Get normalized TPS performance as percentage (0-100%) + /// + public static float GetNormalizedTPS(float currentTps) + { + float targetTps = GetTargetTPS(); + + if (targetTps == 0f) return 100f; + + float percentage = (currentTps / targetTps) * 100f; + return Math.Min(percentage, 100f); + } + + /// + /// Get normalized TPS performance or null if in stabilization period + /// + public static float? GetStableNormalizedTPS(float currentTps) + { + if (IsInStabilizationPeriod()) + return null; + + return GetNormalizedTPS(currentTps); + } + + /// + /// Unified color logic for performance metrics + /// + public static Color GetPerformanceColor(float value, float goodThreshold, float moderateThreshold, bool lowerIsBetter = false) + { + return lowerIsBetter switch + { + false when value >= goodThreshold => Color.green, + false when value >= moderateThreshold => Color.yellow, + true when value <= goodThreshold => Color.green, + true when value <= moderateThreshold => Color.yellow, + _ => Color.red + }; + } + + private static bool IsForcedNormal() => Find.TickManager?.slower?.ForcedNormalSpeed == true; + + private static float CalculateTPS(float rateMultiplier, TimeSpeed speed) + { + if (speed == TimeSpeed.Paused || rateMultiplier == 0f) return 0f; + if (IsForcedNormal()) return 60f; + return rateMultiplier * 60f; + } + + } +} diff --git a/Source/Client/UI/DebugPanel/PerformanceRecorder.cs b/Source/Client/UI/DebugPanel/PerformanceRecorder.cs index 8338a139..c5d2a220 100644 --- a/Source/Client/UI/DebugPanel/PerformanceRecorder.cs +++ b/Source/Client/UI/DebugPanel/PerformanceRecorder.cs @@ -43,7 +43,7 @@ public static class PerformanceRecorder private static List clientOpinionsSamples = new List(); private static List worldPawnsSamples = new List(); private static List windowCountSamples = new List(); - + public static bool IsRecording => isRecording; public static int FrameCount => recordingFrameCount; public static TimeSpan RecordingDuration => isRecording ? DateTime.Now - recordingStartTime : TimeSpan.Zero; @@ -132,7 +132,13 @@ public static void RecordFrame() var async = Find.CurrentMap.AsyncTime(); float currentTps = IngameUIPatch.tps; tpsSamples.Add(currentTps); - normalizedTpsSamples.Add(StatusBadge.GetNormalizedTPS(currentTps)); + + // Only record normalized TPS if we're not in stabilization period + if (PerformanceCalculator.GetStableNormalizedTPS(currentTps) is float stableNormalizedTps) + { + normalizedTpsSamples.Add(stableNormalizedTps); + } + serverTPTSamples.Add(TickPatch.serverTimePerTick); mapCmdsSamples.Add(async.cmds?.Count ?? 0); worldCmdsSamples.Add(Multiplayer.AsyncWorldTime?.cmds?.Count ?? 0); @@ -144,8 +150,8 @@ public static void RecordFrame() // Memory/system metrics worldPawnsSamples.Add(Find.WorldPawns?.AllPawnsAliveOrDead?.Count ?? 0); windowCountSamples.Add(Find.WindowStack?.windows?.Count ?? 0); - } - + } + private static PerformanceResults GenerateResults(TimeSpan duration) { return new PerformanceResults @@ -234,7 +240,7 @@ private static void OutputToFile(PerformanceResults results) { try { - var fileName = $"mp_performance_{results.StartTime:yyyyMMdd_HHmmss}.txt"; + var fileName = $"MpLogs/MpPerf-{results.StartTime:MMddHHmmss}.txt"; var filePath = Path.Combine(GenFilePaths.SaveDataFolderPath, fileName); var sb = new StringBuilder(); @@ -255,25 +261,28 @@ private static void OutputToFile(PerformanceResults results) AppendDetailedStats(sb, "Ticks Per Second (Raw)", results.TPS); AppendDetailedStats(sb, "TPS Performance (%)", results.NormalizedTPS); AppendDetailedStats(sb, "Server Time Per Tick (ms)", results.ServerTPT); - sb.AppendLine(); - - sb.AppendLine("NETWORKING METRICS"); - sb.AppendLine("------------------"); AppendDetailedStats(sb, "Timer Lag", results.TimerLag); - AppendDetailedStats(sb, "Received Commands", results.ReceivedCmds); - AppendDetailedStats(sb, "Sent Commands", results.SentCmds); - AppendDetailedStats(sb, "Buffered Changes", results.BufferedChanges); - AppendDetailedStats(sb, "Map Commands", results.MapCmds); - AppendDetailedStats(sb, "World Commands", results.WorldCmds); + sb.AppendLine(); - sb.AppendLine("SYSTEM METRICS"); - sb.AppendLine("--------------"); + + + sb.AppendLine("MISC METRICS"); + sb.AppendLine("------------"); AppendDetailedStats(sb, "Client Opinions", results.ClientOpinions); AppendDetailedStats(sb, "World Pawns", results.WorldPawns); AppendDetailedStats(sb, "Window Count", results.WindowCount); sb.AppendLine(); + sb.AppendLine("COMMAND METRICS"); + sb.AppendLine("---------------"); + AppendCountStats(sb, "Received Commands", (int)results.ReceivedCmds.Max); + AppendCountStats(sb, "Sent Commands", (int)results.SentCmds.Max); + AppendCountStats(sb, "Buffered Changes", (int)results.BufferedChanges.Max); + AppendCountStats(sb, "Map Commands", (int)results.MapCmds.Max); + AppendCountStats(sb, "World Commands", (int)results.WorldCmds.Max); + sb.AppendLine(); + sb.AppendLine("SYSTEM INFORMATION"); sb.AppendLine("------------------"); sb.AppendLine($"RimWorld Version: {VersionControl.CurrentVersionStringWithRev}"); @@ -298,6 +307,10 @@ private static void AppendDetailedStats(StringBuilder sb, string name, StatResul sb.AppendLine($"{name,-25} Avg: {stats.Average,8:F2} Min: {stats.Min,8:F2} Max: {stats.Max,8:F2} Samples: {stats.Count,6:N0}"); } + private static void AppendCountStats(StringBuilder sb, string name, int count) + { + sb.AppendLine($"{name,-25} Count: {count,6:N0}"); + } /// /// Draw performance recorder section for debug panel /// diff --git a/Source/Client/UI/DebugPanel/StatusBadge.cs b/Source/Client/UI/DebugPanel/StatusBadge.cs index 309aae79..f7a4998c 100644 --- a/Source/Client/UI/DebugPanel/StatusBadge.cs +++ b/Source/Client/UI/DebugPanel/StatusBadge.cs @@ -34,55 +34,28 @@ public static StatusBadge GetSyncStatus() public static StatusBadge GetPerformanceStatus() { float tps = IngameUIPatch.tps; - float normalizedTps = GetNormalizedTPS(tps); + + if (PerformanceCalculator.IsInStabilizationPeriod()) + { + return new StatusBadge("▲", Color.yellow, "STAB", "Stabilizing after speed change"); + } + + float normalizedTps = PerformanceCalculator.GetNormalizedTPS(tps); string tooltip = normalizedTps >= 90f ? "Performance is excellent" : normalizedTps >= 70f ? "Performance is good" : normalizedTps >= 50f ? "Performance is moderate" : normalizedTps >= 25f ? "Performance is poor" : "Performance is very poor"; - - return new StatusBadge("▲", GetPerformanceColor(normalizedTps, 90f, 70f), $"{normalizedTps:F0}%", tooltip); - } - - /// - /// Get the target TPS for the current game speed - /// - public static float GetTargetTPS() - { - if (Find.TickManager == null) return 60f; - if (Find.TickManager.Paused) return 0f; - - return Find.TickManager.CurTimeSpeed switch - { - TimeSpeed.Paused => 0f, - TimeSpeed.Normal => 60f, - TimeSpeed.Fast => 180f, - TimeSpeed.Superfast => 360f, - TimeSpeed.Ultrafast => 900f, // Debug mode - _ => 60f - }; - } - - /// - /// Get normalized TPS performance as percentage (0-100%) - /// - public static float GetNormalizedTPS(float currentTps) - { - float targetTps = GetTargetTPS(); - - // If paused, return 100% - if (targetTps == 0f) - return 100f; - float percentage = (currentTps / targetTps) * 100f; - return Math.Min(percentage, 100f); + return new StatusBadge("▲", PerformanceCalculator.GetPerformanceColor(normalizedTps, 90f, 70f), $"{normalizedTps:F0}%", tooltip); } + public static StatusBadge GetTickStatus() { int behind = TickPatch.tickUntil - TickPatch.Timer; - Color color = GetPerformanceColor(behind, 5, 15, true); + Color color = PerformanceCalculator.GetPerformanceColor(behind, 5, 15, true); string tooltip = behind <= 5 ? "Timing is good" : behind <= 15 ? "Slightly behind" : "Significantly behind"; return new StatusBadge("♦", color, behind.ToString(), tooltip); } @@ -99,13 +72,5 @@ public static StatusBadge GetNumOfPlayersStatus() return new StatusBadge("P", playerCount > 0 ? Color.green : Color.red, $"{playerCount}", "Active players in the current map"); } - // Unified color logic for performance metrics - public static Color GetPerformanceColor(float value, float goodThreshold, float moderateThreshold, bool lowerIsBetter = false) - { - if (lowerIsBetter) - return value <= goodThreshold ? Color.green : value < moderateThreshold ? Color.yellow : Color.red; - - return value >= goodThreshold ? Color.green : value >= moderateThreshold ? Color.yellow : Color.red; - } } } diff --git a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs index 1372d9ed..707d58e1 100644 --- a/Source/Client/UI/DebugPanel/SyncDebugPanel.cs +++ b/Source/Client/UI/DebugPanel/SyncDebugPanel.cs @@ -254,7 +254,15 @@ private static float DrawPerformanceRecorderSection(float x, float y, float widt lines.Add(new("Duration:", $"{duration.TotalSeconds:F1}s", Color.white)); lines.Add(new("Avg FPS:", PerformanceRecorder.AverageFPS > 0 ? $"{PerformanceRecorder.AverageFPS:F1}" : "N/A", Color.white)); lines.Add(new("Avg TPS:", PerformanceRecorder.AverageTPS > 0 ? $"{PerformanceRecorder.AverageTPS:F1}" : "N/A", Color.white)); - lines.Add(new("TPS Perf:", PerformanceRecorder.AverageNormalizedTPS > 0 ? $"{PerformanceRecorder.AverageNormalizedTPS:F1}%" : "N/A", Color.white)); + + if (PerformanceCalculator.IsInStabilizationPeriod()) + { + lines.Add(new("TPS Perf:", "STABILIZING", Color.yellow)); + } + else + { + lines.Add(new("TPS Perf:", PerformanceRecorder.AverageNormalizedTPS > 0 ? $"{PerformanceRecorder.AverageNormalizedTPS:F1}%" : "N/A", Color.white)); + } } var section = new DebugSection("PERFORMANCE RECORDER", lines.ToArray()); @@ -339,18 +347,30 @@ private static float DrawPerformanceSection(float x, float y, float width) { // TPS float tps = IngameUIPatch.tps; - float normalizedTps = StatusBadge.GetNormalizedTPS(tps); - float targetTps = StatusBadge.GetTargetTPS(); - Color tpsColor = StatusBadge.GetPerformanceColor(normalizedTps, 90f, 70f); + float targetTps = PerformanceCalculator.GetTargetTPS(); + string tpsPerformanceText; + Color tpsColor; + + if (PerformanceCalculator.IsInStabilizationPeriod()) + { + tpsPerformanceText = "STABILIZING"; + tpsColor = Color.yellow; + } + else + { + float normalizedTps = PerformanceCalculator.GetNormalizedTPS(tps); + tpsPerformanceText = $"{normalizedTps:F1}%"; + tpsColor = PerformanceCalculator.GetPerformanceColor(normalizedTps, 90f, 70f); + } // Frame time float frameTime = Time.deltaTime * 1000f; - Color frameColor = StatusBadge.GetPerformanceColor(frameTime, 20f, 35f, true); + Color frameColor = PerformanceCalculator.GetPerformanceColor(frameTime, 20f, 35f, true); DebugLine[] lines = [ new("Map TPS:", $"{tps:F1}", Color.white), new("Target TPS:", $"{targetTps:F0}", Color.gray), - new("TPS Perf:", $"{normalizedTps:F1}%", tpsColor), + new("TPS Perf:", tpsPerformanceText, tpsColor), new("Frame Time:", $"{frameTime:F1}ms", frameColor), new("Server TPT:", $"{TickPatch.serverTimePerTick:F1}ms", Color.white), new("Avg Frame:", $"{TickPatch.avgFrameTime:F1}ms", Color.white) @@ -427,7 +447,7 @@ private static float DrawTimingSyncSection(float x, float y, float width) try { int timerLag = TickPatch.tickUntil - TickPatch.Timer; - Color lagColor = StatusBadge.GetPerformanceColor(timerLag, 15, 30); + Color lagColor = PerformanceCalculator.GetPerformanceColor(timerLag, 15, 30); DebugLine[] timingLines = [ new("Timer Lag:", $"{timerLag}", lagColor), From 49b68f377c276c1b273f5bcc4384537cf21307d0 Mon Sep 17 00:00:00 2001 From: Reznal Date: Tue, 15 Jul 2025 16:36:34 +1000 Subject: [PATCH 37/38] Plans now sync when: - Colour changes - Places with colour - Copied - Partially copied - Deleted Shuffle now only runs on cells when its deterministic. Serialized Plan --- Source/Client/AsyncTime/AsyncTimeComp.cs | 4 + Source/Client/Patches/Designators.cs | 7 ++ Source/Client/Patches/Determinism.cs | 33 +++++--- Source/Client/Patches/Plans.cs | 82 +++++++++++++++++++ .../Client/Syncing/Dict/SyncDictRimWorld.cs | 22 ++++- Source/Client/Syncing/Game/SyncMethods.cs | 1 + 6 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 Source/Client/Patches/Plans.cs diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 3c645b2b..444d5cb5 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -406,6 +406,8 @@ void RestoreState() if (mode == DesignatorMode.SingleCell) { IntVec3 cell = SyncSerialization.ReadSync(data); + if (designator is Designator_Plan_Add addDesignator) + addDesignator.colorDef = SyncSerialization.ReadSync(data); designator.DesignateSingleCell(cell); designator.Finalize(true); @@ -413,6 +415,8 @@ void RestoreState() else if (mode == DesignatorMode.MultiCell) { IntVec3[] cells = SyncSerialization.ReadSync(data); + if (designator is Designator_Plan_Add addDesignator) + addDesignator.colorDef = SyncSerialization.ReadSync(data); designator.DesignateMultiCell(cells); } diff --git a/Source/Client/Patches/Designators.cs b/Source/Client/Patches/Designators.cs index f082ce98..65b1fff6 100644 --- a/Source/Client/Patches/Designators.cs +++ b/Source/Client/Patches/Designators.cs @@ -25,6 +25,7 @@ public static class DesignatorPatches public static bool DesignateSingleCell(Designator __instance, IntVec3 __0) { if (!Multiplayer.InInterface) return true; + if (__instance is Designator_Plan_Copy or Designator_Plan_CopySelection or Designator_Plan_CopySelectionPaste) return true; Designator designator = __instance; @@ -35,6 +36,9 @@ public static bool DesignateSingleCell(Designator __instance, IntVec3 __0) WriteData(writer, DesignatorMode.SingleCell, designator); SyncSerialization.WriteSync(writer, __0); + if (__instance is Designator_Plan_Add addDesignator) + SyncSerialization.WriteSync(writer, addDesignator.colorDef); + SendSyncCommand(map.uniqueID, writer); Multiplayer.WriterLog.AddCurrentNode(writer); @@ -58,6 +62,9 @@ public static bool DesignateMultiCell(Designator __instance, IEnumerable Transpiler(IEnumerable inst } } - [HarmonyPatch(typeof(Zone), nameof(Zone.Cells), MethodType.Getter)] - static class ZoneCellsShufflePatch + public static class CellsShufflePatchShared { - static FieldInfo CellsShuffled = AccessTools.Field(typeof(Zone), nameof(Zone.cellsShuffled)); - - static IEnumerable Transpiler(IEnumerable insts) + public static IEnumerable Transpiler(IEnumerable insts, FieldInfo cellsShuffledField) { bool found = false; - - foreach (var inst in insts) + foreach (CodeInstruction inst in insts) { yield return inst; - - if (!found && inst.operand == CellsShuffled) + if (!found && inst.operand == cellsShuffledField) { - yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(ZoneCellsShufflePatch), nameof(ShouldShuffle))); + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(CellsShufflePatchShared), nameof(ShouldShuffle))); yield return new CodeInstruction(OpCodes.Not); yield return new CodeInstruction(OpCodes.Or); found = true; @@ -170,12 +165,28 @@ static IEnumerable Transpiler(IEnumerable inst } } - static bool ShouldShuffle() + public static bool ShouldShuffle() { return Multiplayer.Client == null || Multiplayer.Ticking; } } + [HarmonyPatch(typeof(Zone), nameof(Zone.Cells), MethodType.Getter)] + static class ZoneCellsShufflePatch + { + static readonly FieldInfo CellsShuffled = AccessTools.Field(typeof(Zone), "cellsShuffled"); + static IEnumerable Transpiler(IEnumerable insts) + => CellsShufflePatchShared.Transpiler(insts, CellsShuffled); + } + + [HarmonyPatch(typeof(Plan), nameof(Plan.Cells), MethodType.Getter)] + static class PlanCellsShufflePatch + { + static readonly FieldInfo CellsShuffled = AccessTools.Field(typeof(Plan), "cellsShuffled"); + static IEnumerable Transpiler(IEnumerable insts) + => CellsShufflePatchShared.Transpiler(insts, CellsShuffled); + } + [HarmonyPatch] static class SortArchivablesById { diff --git a/Source/Client/Patches/Plans.cs b/Source/Client/Patches/Plans.cs new file mode 100644 index 00000000..0441ee4c --- /dev/null +++ b/Source/Client/Patches/Plans.cs @@ -0,0 +1,82 @@ +using HarmonyLib; +using Multiplayer.API; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace Multiplayer.Client.Patches +{ + [HarmonyPatch] + public static class PlanGetGizmosPatch + { + static IEnumerable TargetMethods() + { + Type planType = AccessTools.TypeByName("Verse.Plan+<>c__DisplayClass45_0"); + yield return AccessTools.Method(planType, "b__1"); + } + + static bool Prefix(object __instance) + { + if (Multiplayer.Client == null) return true; + + Plan plan = (Plan)AccessTools.Field(__instance.GetType(), "<>4__this").GetValue(__instance); + ColorDef color = (ColorDef)AccessTools.Field(__instance.GetType(), "newCol").GetValue(__instance); + + SyncSetPlanColor(plan, color); + return false; + } + + [SyncMethod] + public static void SyncSetPlanColor(Plan plan, ColorDef color) + { + plan.Color = color; + AccessTools.Method(typeof(Plan), "CheckContiguous").Invoke(plan, null); + } + } + + [HarmonyPatch(typeof(Designator_Plan_Copy), nameof(Designator_Plan_Copy.DesignateSingleCell))] + public static class PlanCopySingleCellPatch + { + static bool Prefix(Designator_Plan_Copy __instance, IntVec3 c) + { + if (Multiplayer.Client == null) return true; + + SyncDesignateSingleCell(__instance, __instance.cells, __instance.colorDef); + return false; + } + + [SyncMethod] + public static void SyncDesignateSingleCell(Designator_Plan_Add designator, List cells, ColorDef colorDef) + { + designator.colorDef = colorDef; + designator.PlanCells(cells); + } + } + + [HarmonyPatch(typeof(Designator_Plan_CopySelectionPaste), nameof(Designator_Plan_CopySelectionPaste.DesignateSingleCell))] + public static class PlanCopySelectionPasteSingleCellPatch + { + static bool Prefix(Designator_Plan_CopySelectionPaste __instance, IntVec3 c) + { + if (Multiplayer.Client == null) return true; + + foreach (ColorDef color in __instance.colors) + SyncDesignateSingleCell(__instance, __instance.GetCurrentCells(c, color).ToList(), color); + + return false; + } + + [SyncMethod] + public static void SyncDesignateSingleCell(Designator_Plan_CopySelectionPaste designator, List cells, ColorDef colorDef) + { + designator.SelectedPlan = null; + designator.colorDef = colorDef; + designator.PlanCells(cells); + } + } +} diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index f7c3817d..cb0fa722 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -1026,7 +1026,7 @@ public static class SyncDictRimWorld { // TODO: Consider using int16 rather that int32 to minimize network traffic. // Investigate if the tiles/layers are small enough to allow that. - (ByteWriter data, PlanetTile tile) => + (ByteWriter data, PlanetTile tile) => { data.WriteInt32(tile.tileId); data.WriteInt32(tile.layerId); @@ -1095,6 +1095,26 @@ public static class SyncDictRimWorld return data.MpContext().map.zoneManager.AllZones.Find(zone => zone.ID == zoneId); }, true }, + { + (ByteWriter data, Plan plan) => + { + if (plan == null) + { + data.WriteInt32(-1); + } + else + { + data.MpContext().map = plan.Map; + data.WriteInt32(plan.ID); + } + }, + (ByteReader data) => { + int zoneId = data.ReadInt32(); + if (zoneId == -1) + return null; + return data.MpContext().map.planManager.AllPlans.Find(p => p.ID == zoneId); + }, true + }, #endregion #region Globals diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index 932066bb..fa682c69 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -42,6 +42,7 @@ public static void Init() SyncMethod.Register(typeof(Pawn_GuestTracker), nameof(Pawn_GuestTracker.SetExclusiveInteraction)).CancelIfAnyArgNull(); SyncMethod.Register(typeof(Pawn_GuestTracker), nameof(Pawn_GuestTracker.ToggleNonExclusiveInteraction)).CancelIfAnyArgNull(); SyncMethod.Register(typeof(Zone), nameof(Zone.Delete)); + SyncMethod.Register(typeof(Plan), nameof(Plan.Delete)); SyncMethod.Register(typeof(BillStack), nameof(BillStack.AddBill)).ExposeParameter(0); // Only used for pasting SyncMethod.Register(typeof(BillStack), nameof(BillStack.Delete)).CancelIfAnyArgNull().SetPostInvoke(TryDirtyCurrentPawnTable); SyncMethod.Register(typeof(BillStack), nameof(BillStack.Reorder)).CancelIfAnyArgNull(); From 06f3eb2580ba53799c6aa704e97f432eb092dfa9 Mon Sep 17 00:00:00 2001 From: Reznal Date: Wed, 16 Jul 2025 19:27:02 +1000 Subject: [PATCH 38/38] Removed saveable mp logs --- Source/Client/Multiplayer.cs | 1 - Source/Client/Util/MpLog.cs | 3 +- Source/Client/Util/SaveableMpLogs.cs | 183 --------------------------- 3 files changed, 1 insertion(+), 186 deletions(-) delete mode 100644 Source/Client/Util/SaveableMpLogs.cs diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 8c5efafd..c8093061 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -187,7 +187,6 @@ public static void StopMultiplayerAndClearAllWindows() public static void StopMultiplayer() { Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); - SaveableMpLogs.ResetMpLogs(); OnMainThread.ClearScheduled(); LongEventHandler.ClearQueuedEvents(); diff --git a/Source/Client/Util/MpLog.cs b/Source/Client/Util/MpLog.cs index 05013ecd..1793c18e 100644 --- a/Source/Client/Util/MpLog.cs +++ b/Source/Client/Util/MpLog.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Multiplayer.Client.Util { @@ -23,7 +23,6 @@ public static void Error(string msg) public static void Debug(string msg) { Verse.Log.Message($"{Multiplayer.username} {TickPatch.Timer} {msg}"); - SaveableMpLogs.AddLog("DEBUG", msg); } } } diff --git a/Source/Client/Util/SaveableMpLogs.cs b/Source/Client/Util/SaveableMpLogs.cs deleted file mode 100644 index 3a658e05..00000000 --- a/Source/Client/Util/SaveableMpLogs.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using HarmonyLib; -using Multiplayer.Common; -using RimWorld; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client.Util; - -public class SaveableMpLogs -{ - private const int MaxFiles = 10; - private const string FilePrefix = "MpLog-"; - private const string FileExtension = ".log"; - - private static string _currentLogFile = null; - - public static void InitMpLogs() - { - try - { - _currentLogFile = FindFileNameForNextFile(); - - if (string.IsNullOrEmpty(_currentLogFile)) - { - Log.Warning("SaveableMpLogs: Could not determine log file name, skipping log initialization"); - return; - } - - using var stream = File.Open(_currentLogFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); - using var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine(GetLogDetails()); - } - catch (Exception e) - { - Log.Error($"SaveableMpLogs: Exception writing initial log info: {e}"); - _currentLogFile = null; // Reset on error - } - } - - public static void ResetMpLogs() => _currentLogFile = null; - - public static void AddLog(string type, string logText) - { - try - { - if (Multiplayer.Client == null) - return; - - if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(logText)) - { - Log.Warning("SaveableMpLogs: Invalid log parameters, skipping log entry"); - return; - } - - if (_currentLogFile == null) - { - InitMpLogs(); - if (_currentLogFile == null) // Still null after init attempt - return; - } - - int ticks = Multiplayer.Client == null ? -1 : Find.TickManager?.TicksGame ?? -1; - int mapTicks = Find.CurrentMap?.AsyncTime()?.mapTicks ?? -1; - - using var stream = File.Open(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); - using var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine($"[{type}] [{ticks}] [{mapTicks}] {logText}"); - } - catch (Exception e) - { - Log.Error($"SaveableMpLogs: Exception writing log info: {e}"); - // Don't reset _currentLogFile here as it might be a temporary file system issue - } - } - - private static string GetLogDetails() - { - try - { - var logDetails = new StringBuilder() - .AppendLine($"Multiplayer Log - {DateTime.Now}") - .AppendLine("\n###Version Data###") - .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}") - .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}") - .AppendLine("\n###Debug Options###") - .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}") - .AppendLine($"Multiplayer Debug Mode - Host|||{Multiplayer.GameComp?.debugMode ?? false}") - .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}") - .AppendLine("\n###Server Info###") - .AppendLine($"Async time active|||{Multiplayer.GameComp?.asyncTime ?? false}") - .AppendLine($"Multifaction active|||{Multiplayer.GameComp?.multifaction ?? false}") - .AppendLine("\n###OS Info###") - .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}") - .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}") - .AppendLine("\n======================================================") - .AppendLine("###Log Start###") - .AppendLine("======================================================"); - return logDetails.ToString(); - } - catch (Exception e) - { - Log.Error($"SaveableMpLogs: Exception getting log details: {e}"); - return $"Multiplayer Log - {DateTime.Now}\nError getting full log details: {e.Message}\n======================================================"; - } - } - - private static string FindFileNameForNextFile() - { - try - { - // Get player directory - string directory = Path.Combine(Multiplayer.MpLogsDir); - - // Ensure the directory exists - Directory.CreateDirectory(directory); - - // Get all existing logs - FileInfo[] files = new DirectoryInfo(directory).GetFiles($"{FilePrefix}*{FileExtension}"); - - // Delete any pushing us over the limit, and reserve room for one more - if (files.Length > MaxFiles - 1) - { - try - { - files.OrderByDescending(f => f.LastWriteTime).Skip(MaxFiles - 1).Do(DeleteFileSilent); - } - catch (Exception e) - { - Log.Warning($"SaveableMpLogs: Exception cleaning up old log files: {e}"); - } - } - - // Find the current max number - int max = 0; - foreach (FileInfo file in files) - { - try - { - // Get name without extension and prefix - string fileName = Path.GetFileNameWithoutExtension(file.Name); - if (fileName.Length > FilePrefix.Length) - { - string parsedName = fileName[FilePrefix.Length..]; - - // Try to parse the number and update max if it's greater - if (int.TryParse(parsedName, out int result) && result > max) - max = result; - } - } - catch (Exception e) - { - Log.Warning($"SaveableMpLogs: Exception processing file {file.Name}: {e}"); - } - } - - return Path.Combine(directory, $"{FilePrefix}{max + 1:00}{FileExtension}"); - } - catch (Exception e) - { - Log.Error($"SaveableMpLogs: Exception finding log file name: {e}"); - return null; - } - } - - private static void DeleteFileSilent(FileInfo file) - { - try - { - if (file?.Exists == true) - { - file.Delete(); - } - } - catch (Exception) - { - // Silently ignore all exceptions when deleting old log files - } - } -}