diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 8071241c..c7e7b1cf 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -14,6 +14,7 @@ using System.Threading; using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Client.Patches; using Multiplayer.Client.Util; namespace Multiplayer.Client @@ -216,6 +217,7 @@ public static void StopMultiplayer() game = null; TickPatch.Reset(); + VTRSync.Reset(); Find.WindowStack?.WindowOfType()?.Cleanup(true); SyncFieldUtil.ClearAllBufferedChanges(); diff --git a/Source/Client/Patches/VTRSyncPatch.cs b/Source/Client/Patches/VTRSyncPatch.cs index f0d0713f..b9282f35 100644 --- a/Source/Client/Patches/VTRSyncPatch.cs +++ b/Source/Client/Patches/VTRSyncPatch.cs @@ -56,41 +56,54 @@ static class VTRSync { // 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; + public const int InvalidMapIndex = -1; + public static int lastMovedToMap = InvalidMapIndex; + public static int lastSentAtTick = -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; + public static int GetSynchronizedUpdateRate(Thing thing) => thing?.MapHeld?.AsyncTime()?.VTR ?? MaximumVtr; + + public static void SendViewedMapUpdate(int previous, int current) + { + string warn = string.Empty; + if (previous != lastMovedToMap) + warn = $" mismatch between expected previous map {previous} and last moved to map {lastMovedToMap}"; + else if (previous == current) return; + int currentTick = Find.TickManager?.TicksGame ?? 0; + MpLog.Debug($"VTR MapSwitchPatch: {lastMovedToMap}->{current} @ tick {currentTick}{warn}"); + Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(previous, current)); + lastMovedToMap = current; + } + + public static void Reset() + { + lastMovedToMap = InvalidMapIndex; + } } [HarmonyPatch(typeof(Game), nameof(Game.CurrentMap), MethodType.Setter)] static class MapSwitchPatch { - const int InvalidMapIndex = -1; - static void Prefix(Map value) { - if (Multiplayer.Client == null || Client.Multiplayer.session == null) return; + if (Multiplayer.Client == null) return; try { + // WorldRenderModePatch will handle it + if (VTRSync.lastMovedToMap == VTRSync.WorldMapId) return; int previousMap = GetPreviousMapIndex(); - int newMap = value?.uniqueID ?? InvalidMapIndex; + int newMap = value?.uniqueID ?? VTRSync.InvalidMapIndex; int currentTick = Find.TickManager?.TicksGame ?? 0; - if (previousMap == newMap) - return; - - if (VTRSync.lastMovedToMap == newMap && currentTick == VTRSync.lastSentTick) - return; + if (previousMap == newMap) return; + if (VTRSync.lastMovedToMap == newMap && currentTick == VTRSync.lastSentAtTick) return; - MpLog.Debug($"VTR MapSwitchPatch: Switching from map {previousMap} to {newMap} at tick {currentTick}"); - Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(previousMap, newMap)); - VTRSync.lastMovedToMap = newMap; - VTRSync.lastSentTick = currentTick; + VTRSync.SendViewedMapUpdate(previousMap, newMap); + VTRSync.lastSentAtTick = currentTick; } catch (Exception ex) { @@ -104,10 +117,10 @@ private static int GetPreviousMapIndex() if (currentMapIsRemovedAndWasLatestMap) { - return InvalidMapIndex; + return VTRSync.InvalidMapIndex; } - return Find.CurrentMap?.uniqueID ?? InvalidMapIndex; + return Find.CurrentMap?.uniqueID ?? VTRSync.InvalidMapIndex; } } @@ -125,9 +138,18 @@ static void Postfix(WorldRenderMode __result) // Detect transition to world map (Planet mode) if (__result == WorldRenderMode.Planet && lastRenderMode != WorldRenderMode.Planet) { - if (VTRSync.lastMovedToMap != -1) + if (VTRSync.lastMovedToMap != VTRSync.InvalidMapIndex && VTRSync.lastMovedToMap != VTRSync.WorldMapId) + { + VTRSync.SendViewedMapUpdate(VTRSync.lastMovedToMap, VTRSync.WorldMapId); + } + } + // Detect transition back to tile map + else if (__result != WorldRenderMode.Planet && lastRenderMode == WorldRenderMode.Planet) + { + var newMap = Find.CurrentMap?.uniqueID ?? VTRSync.InvalidMapIndex; + if (newMap != VTRSync.InvalidMapIndex && VTRSync.lastMovedToMap == VTRSync.WorldMapId) { - Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(VTRSync.lastMovedToMap, VTRSync.WorldMapId)); + VTRSync.SendViewedMapUpdate(VTRSync.WorldMapId, newMap); } } diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index 49f4097e..3c9f9f1e 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -48,6 +48,16 @@ public void HandleClientCommand(ByteReader data) int mapId = data.ReadInt32(); byte[]? extra = data.ReadPrefixedBytes(65535); if (extra == null) return; + if (cmd == CommandType.PlayerCount) + { + ByteReader reader = new ByteReader(extra); + var prevMap = reader.ReadInt32(); + var newMap = reader.ReadInt32(); + if (Player.currentMap != prevMap) + ServerLog.Error($"Inconsistent player {Player.Username} map. Last known map: {Player.currentMap}, " + + $"however received command with transition: {prevMap} -> {newMap}"); + Player.currentMap = newMap; + } // todo check if map id is valid for the player @@ -110,9 +120,6 @@ 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); writer.WriteByte(map); diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index 2c43957e..aacd3ff2 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -83,15 +83,11 @@ 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 the player from their last known map + if (player.currentMap != -1) { - // 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); - } + 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 diff --git a/Source/Common/ServerPlayer.cs b/Source/Common/ServerPlayer.cs index ea45a5b5..14b35e42 100644 --- a/Source/Common/ServerPlayer.cs +++ b/Source/Common/ServerPlayer.cs @@ -30,7 +30,7 @@ public class ServerPlayer : IChatSource public bool frozen; public int unfrozenAt; - // Track which map the player is currently on (from cursor updates) + // Track which map the player is currently on public int currentMap = -1; public string Username => conn.username;