From 17df8b383d50dd3e93e6d0c6042a25a10c55c177 Mon Sep 17 00:00:00 2001 From: Alexey Nikitin Date: Tue, 4 Aug 2026 20:03:45 -0500 Subject: [PATCH] Replay debug actions under the acting player's world view A debug action replayed on another client re-derives whatever it needs from local state. DebugSync already reproduces the cursor and the selected object, but not which view the player had open -- and vanilla's incident action picks its entire target from exactly that: WorldSelected ? Find.WorldSelector.SingleSelectedObject : Find.CurrentMap So a host on the planet with a caravan selected fires at the caravan, while a client looking at a colony fires at the map. The node's label is built from the target's name, so the two clients also disagree about the label and RecreateGraphAndGetNode finds nothing -- the command is accepted, acknowledged, and silently never executed there. A synchronized command that runs on one client and not the other is a desync, and this one leaves no trace where it happens. Carry the flag in the debug command and override the getter during replay, following the MouseCellPatch and MouseTilePatch pattern already used for the cursor. Read it before the node graph is rebuilt, since labels depend on it, and clear it in the same finally block as the others so it cannot leak into normal play. The debug command's wire format gains one byte; host and clients need matching builds, which this mod already requires. --- Source/Client/Debug/DebugSync.cs | 12 ++++++++++++ Source/Client/Patches/Patches.cs | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Source/Client/Debug/DebugSync.cs b/Source/Client/Debug/DebugSync.cs index 4b8609d8c..aaea73357 100644 --- a/Source/Client/Debug/DebugSync.cs +++ b/Source/Client/Debug/DebugSync.cs @@ -48,6 +48,12 @@ public static void HandleCmd(ByteReader data) int selectedId = data.ReadInt32(); + // Replay under the acting player's view, not the local one. Read before the node graph is + // rebuilt below, because node labels are allowed to depend on it -- the incident action's label + // embeds its target's name, so a wrong view here produces a path that matches nothing and the + // command silently does not run on this client. + WorldSelectedPatch.result = data.ReadBool(); + if (Multiplayer.MapContext != null) { var thing = Multiplayer.ThingsById.GetValueSafe(selectedId); @@ -115,6 +121,7 @@ public static void HandleCmd(ByteReader data) MouseCellPatch.result = null; MouseTilePatch.result = null; + WorldSelectedPatch.result = null; Find.Selector.selected = prevSelected; FieldRefs.worldSelected(Find.WorldSelector) = prevWorldSelected; @@ -156,6 +163,11 @@ public static void SendCmd(DebugSource source, int hash, string path, Map map, I else writer.WriteInt32(Find.WorldSelector.SingleSelectedObject?.ID ?? -1); + // Which view the acting player had open. Debug actions may legitimately read it -- vanilla's + // incident action derives its entire target from it -- so replaying one faithfully means + // reproducing it, exactly as the cursor and selection above are reproduced. + writer.WriteBool(WorldRendererUtility.WorldSelected); + Multiplayer.WriterLog.AddCurrentNode(writer); int mapId = map?.uniqueID ?? ScheduledCommand.Global; diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 344833379..d6127fe52 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -192,6 +192,29 @@ static void Postfix(ref PlanetTile __result) } } + /// + /// Overrides whether the planet view is showing, while a debug command is being replayed. + /// + /// Companion to the cursor overrides above. Debug actions are allowed to read the interface -- vanilla's + /// incident action picks its target with + /// WorldRendererUtility.WorldSelected ? Find.WorldSelector.SingleSelectedObject : Find.CurrentMap -- + /// so replaying one faithfully means reproducing what the acting player could see, not just where their + /// cursor was. Without this, the same command targets a caravan on a player looking at the planet and a + /// colony on a player looking at a map. + /// + [HarmonyPatch(typeof(WorldRendererUtility), nameof(WorldRendererUtility.WorldSelected), MethodType.Getter)] + public static class WorldSelectedPatch + { + /// Non-null only while a debug command is being replayed. + public static bool? result; + + static void Postfix(ref bool __result) + { + if (result.HasValue) + __result = result.Value; + } + } + [HarmonyPatch(typeof(KeyBindingDef), nameof(KeyBindingDef.IsDownEvent), MethodType.Getter)] public static class KeyIsDownPatch {