Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Source/Client/AsyncTime/AsyncTimeComp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Multiplayer.Client.Patches;
using Multiplayer.Client.Saving;
using Multiplayer.Client.Util;
using System.Linq;

namespace Multiplayer.Client
{
Expand Down Expand Up @@ -98,6 +99,9 @@ public int GameStartAbsTick

public Queue<ScheduledCommand> cmds = new();

public int CurrentPlayerCount { get; private set; } = 0;
public int VTR => CurrentPlayerCount > 0 ? VTRSync.MinimumVtr : VTRSync.MaximumVtr;

public AsyncTimeComp(Map map, int gameStartAbsTick = 0)
{
this.map = map;
Expand Down Expand Up @@ -228,6 +232,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<ScheduledCommand>(
Expand Down
13 changes: 13 additions & 0 deletions Source/Client/AsyncTime/AsyncWorldTimeComp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,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)
{
Expand Down
134 changes: 134 additions & 0 deletions Source/Client/Patches/VTRSyncPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using HarmonyLib;
using Multiplayer.Client.Patches;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using RimWorld.Planet;
using System;
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 = 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;

__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 = VTRSync.MaximumVtr;
return false;
}
}

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;

// Vtr rates
public const int MaximumVtr = 15;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just noticed these are already defined in Thing, we could probably use those.

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)]
static class MapSwitchPatch
{
static void Prefix(Map value)
{
if (Multiplayer.Client == null || Client.Multiplayer.session == null) return;

try
{
int previousMap = Find.CurrentMap?.uniqueID ?? -1;
int newMap = value?.uniqueID ?? -1;
int currentTick = Find.TickManager?.TicksGame ?? 0;

// 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 (VTRSync.lastMovedToMap == previousMap && currentTick == VTRSync.lastSentTick)
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
VTRSync.lastMovedToMap = newMap;
VTRSync.lastSentTick = currentTick;
}
catch (Exception ex)
{
MpLog.Error($"VTR MapSwitchPatch error: {ex.Message}");
}
}
}

[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}");
}
}
}
}
1 change: 1 addition & 0 deletions Source/Common/CommandType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ public enum CommandType : byte
// Map scope
MapTimeSpeed,
Designator,
PlayerCount,
}
3 changes: 3 additions & 0 deletions Source/Common/Networking/State/ServerPlayingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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);
Expand Down
10 changes: 10 additions & 0 deletions Source/Common/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 3 additions & 0 deletions Source/Common/ServerPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading