Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions Source/Client/AsyncTime/AsyncTimeComp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ public void ExposeData()
// nevertheless still left.
public int DecreasePlayerCount() => CurrentPlayerCount = Math.Max(0, CurrentPlayerCount - 1);

public void SetCurrentPlayerCount(int count) => CurrentPlayerCount = Math.Max(0, count);

public void FinalizeInit()
{
cmds = new Queue<ScheduledCommand>(
Expand Down
8 changes: 8 additions & 0 deletions Source/Client/AsyncTime/AsyncWorldTimeComp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public TimeSpeed DesiredTimeSpeed
public int CurrentPlayerCount { get; private set; }
public int VTR => CurrentPlayerCount > 0 ? VTRSync.MinimumVtr : VTRSync.MaximumVtr;

public void SetCurrentPlayerCount(int count) => CurrentPlayerCount = Math.Max(0, count);

public int TickableId => -1;

public World world;
Expand Down Expand Up @@ -295,6 +297,12 @@ private static void CreateJoinPointAndSendIfHost()
// Hosted: only host/arbiter uploads world data
SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
}

if (!Multiplayer.IsReplay)
{
Patches.VTRSync.ReportCurrentViewedMap();
Patches.VTRSync.RequestPlayerCountsSync();
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions Source/Client/Networking/HostUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public static async ClientTask HostServer(ServerSettings settings, bool fromRepl
SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, false);

StartLocalServer();

Patches.VTRSync.ReportCurrentViewedMap();
Patches.VTRSync.RequestPlayerCountsSync();
}

private static void CreateSession(ServerSettings settings) =>
Expand Down
5 changes: 5 additions & 0 deletions Source/Client/Networking/State/ClientLoadingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,10 @@ public void HandleWorldData(ByteReader data)
var loadingMs = watch.ElapsedMilliseconds;
Log.Message($"Loaded game in {loadingMs}ms");
connection.ChangeState(ConnectionStateEnum.ClientPlaying);
if (!Multiplayer.IsReplay)
{
OnMainThread.Enqueue(Patches.VTRSync.ReportCurrentViewedMap);
OnMainThread.Enqueue(Patches.VTRSync.RequestPlayerCountsSync);
}
}
}
18 changes: 18 additions & 0 deletions Source/Client/Networking/State/ClientPlayingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ public void HandleSetFaction(ServerSetFactionPacket packet)
Session.myFactionId = factionId;
}
}

[TypedPacketHandler]
public void HandlePlayerCounts(ServerPlayerCountsPacket packet)
{
var countById = new Dictionary<int, int>();
int len = Math.Min(packet.mapIds?.Length ?? 0, packet.counts?.Length ?? 0);
for (int i = 0; i < len; i++)
countById[packet.mapIds[i]] = packet.counts[i];

foreach (var map in Find.Maps)
{
if (countById.TryGetValue(map.uniqueID, out int count))
map.AsyncTime().SetCurrentPlayerCount(count);
}

if (countById.TryGetValue(Patches.VTRSync.WorldMapId, out int worldCount))
Multiplayer.AsyncWorldTime.SetCurrentPlayerCount(worldCount);
}
}

}
45 changes: 44 additions & 1 deletion Source/Client/Patches/VTRSyncPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using HarmonyLib;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld.Planet;
using Verse;

Expand Down Expand Up @@ -48,7 +49,7 @@ static bool Prefix(ref int __result, WorldObject __instance)
}
}

static class VTRSync
public static class VTRSync
{
// Special identifier for the world map (since it doesn't have a uniqueID like regular maps)
public const int WorldMapId = -2;
Expand All @@ -75,6 +76,48 @@ public static void SendViewedMapUpdate(int previous, int current)
MpLog.Debug($"VTR MapSwitchPatch: {lastMovedToMapId}->{current} @ tick {currentTick}{warn}");
Multiplayer.Client.SendCommand(CommandType.PlayerCount, ScheduledCommand.Global, ByteWriter.GetBytes(previous, current));
lastMovedToMapId = current;

ReportViewedMap(current);
}

public static void ReportViewedMap(int mapId)
{
var client = Multiplayer.Client;
if (client == null || Multiplayer.IsReplay)
return;

if (client.State != ConnectionStateEnum.ClientPlaying)
return;

int currentTick = Find.TickManager?.TicksGame ?? 0;
MpLog.Debug($"VTR report: map={mapId} @ tick {currentTick}");
lastMovedToMapId = mapId;
client.Send(new ClientViewedMapReportPacket { mapId = mapId });
}

public static void ReportCurrentViewedMap()
{
if (Multiplayer.Client == null || Multiplayer.IsReplay)
return;

int current = WorldRendererUtility.CurrentWorldRenderMode == WorldRenderMode.Planet
? WorldMapId
: Find.CurrentMap?.uniqueID ?? InvalidMapId;

ReportViewedMap(current);
}

public static void RequestPlayerCountsSync()
{
var client = Multiplayer.Client;
if (client == null || Multiplayer.IsReplay)
return;

if (client.State != ConnectionStateEnum.ClientPlaying)
return;

MpLog.Debug($"VTR resync request @ tick {Find.TickManager?.TicksGame ?? 0}");
client.Send(new ClientRequestPlayerCountsPacket());
}

public static void Reset()
Expand Down
6 changes: 6 additions & 0 deletions Source/Client/Saving/Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ private static void PostLoad()
Multiplayer.AsyncWorldTime.cmds = new Queue<ScheduledCommand>(
Multiplayer.session.dataSnapshot.MapCmds.GetValueSafe(ScheduledCommand.Global) ?? []);
// Map cmds are added in MapAsyncTimeComp.FinalizeInit

if (!Multiplayer.IsReplay && Multiplayer.Client is ClientPlayingState)
{
OnMainThread.Enqueue(Patches.VTRSync.ReportCurrentViewedMap);
OnMainThread.Enqueue(Patches.VTRSync.RequestPlayerCountsSync);
}
}

private static XmlDocument DataSnapshotToXml(GameDataSnapshot dataSnapshot, List<int> mapsToLoad)
Expand Down
33 changes: 33 additions & 0 deletions Source/Common/Networking/Packet/ViewedMapPackets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Multiplayer.Common.Networking.Packet;

[PacketDefinition(Packets.Client_ViewedMapReport)]
public record struct ClientViewedMapReportPacket : IPacket
{
public int mapId;

public void Bind(PacketBuffer buf)
{
buf.Bind(ref mapId);
}
}

[PacketDefinition(Packets.Client_RequestPlayerCounts)]
public record struct ClientRequestPlayerCountsPacket : IPacket
{
public void Bind(PacketBuffer buf)
{
}
}

[PacketDefinition(Packets.Server_PlayerCounts)]
public record struct ServerPlayerCountsPacket : IPacket
{
public int[] mapIds;
public int[] counts;

public void Bind(PacketBuffer buf)
{
buf.Bind(ref mapIds, BinderOf.Int());
buf.Bind(ref counts, BinderOf.Int());
}
}
4 changes: 4 additions & 0 deletions Source/Common/Networking/Packets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public enum Packets : byte
Client_FrameTime,
Client_StandaloneWorldSnapshotUpload,
Client_StandaloneMapSnapshotUpload,
Client_ViewedMapReport,
Client_RequestPlayerCounts,

// Joining
Server_ProtocolOk,
Expand Down Expand Up @@ -65,6 +67,8 @@ public enum Packets : byte
Server_SetFaction,
Server_RequestRejoin,

Server_PlayerCounts,

// All states (Joining, Loading, Playing)
Server_Disconnect,

Expand Down
32 changes: 32 additions & 0 deletions Source/Common/Networking/State/ServerPlayingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,38 @@ public void HandleClientCommand(ClientCommandPacket packet)
Server.SendMapResponse(Player, currentMapId);
}

[TypedPacketHandler]
public void HandleViewedMapReport(ClientViewedMapReportPacket packet)
{
Player.currentMapId = packet.mapId;
Player.hasReportedCurrentMap = true;
}

[TypedPacketHandler]
public void HandleRequestPlayerCounts(ClientRequestPlayerCountsPacket packet)
{
var countsByMap = new Dictionary<int, int>();
foreach (var player in Server.PlayingPlayers)
{
if (player.currentMapId == -1)
continue;
countsByMap.TryGetValue(player.currentMapId, out int count);
countsByMap[player.currentMapId] = count + 1;
}

var mapIds = new int[countsByMap.Count];
var counts = new int[countsByMap.Count];
int i = 0;
foreach (var kv in countsByMap)
{
mapIds[i] = kv.Key;
counts[i] = kv.Value;
i++;
}

Player.SendPacket(new ServerPlayerCountsPacket { mapIds = mapIds, counts = counts });
}

public const int MaxChatMsgLength = 128;

[TypedPacketHandler]
Expand Down
2 changes: 1 addition & 1 deletion Source/Common/Version.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Multiplayer.Common
public static class MpVersion
{
public const string SimpleVersion = "0.11.5";
public const int Protocol = 56;
public const int Protocol = 57;

public static readonly string? GitHash = Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>()
Expand Down
111 changes: 111 additions & 0 deletions Source/Tests/ViewedMapSyncTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;

namespace Tests;

[TestFixture]
public class ViewedMapSyncTest
{
private MultiplayerServer server = null!;
private int nextPlayerId;

[SetUp]
public void SetUp()
{
ServerLog.error = (msg) => TestContext.Error.WriteLine(msg);
server = MultiplayerServer.instance = new MultiplayerServer(new ServerSettings
{
gameName = "Test",
direct = false,
lan = false
});
nextPlayerId = 1;
}

[TearDown]
public void TearDown()
{
MultiplayerServer.instance = null;
}

private (ServerPlayer player, RecordingConnection conn) AddPlayer(string username, int currentMapId,
bool hasReportedCurrentMap = true)
{
var conn = new RecordingConnection(username);
var player = new ServerPlayer(nextPlayerId++, conn)
{
currentMapId = currentMapId,
hasReportedCurrentMap = hasReportedCurrentMap,
};
conn.serverPlayer = player;
conn.ChangeState(ConnectionStateEnum.ServerPlaying);
server.playerManager.Players.Add(player);
return (player, conn);
}

private ServerPlayingState PlayingState(ServerPlayer player) =>
player.conn.GetState<ServerPlayingState>()!;

[Test]
public void ViewedMapReport_SetsMapAbsolutely_AndIsIdempotent()
{
var (player, conn) = AddPlayer("player", -1, hasReportedCurrentMap: false);

PlayingState(player).HandleViewedMapReport(new ClientViewedMapReportPacket { mapId = 7 });
Assert.That(player.currentMapId, Is.EqualTo(7));
Assert.That(player.hasReportedCurrentMap, Is.True);

PlayingState(player).HandleViewedMapReport(new ClientViewedMapReportPacket { mapId = 7 });
Assert.That(player.currentMapId, Is.EqualTo(7));
}

[Test]
public void ViewedMapReport_DoesNotGenerateAnyPacket()
{
server.worldData.mapData[3] = [1, 2, 3];
var (player, conn) = AddPlayer("player", 3);

PlayingState(player).HandleViewedMapReport(new ClientViewedMapReportPacket { mapId = 4 });

Assert.That(conn.SentPackets, Is.Empty);
}

[Test]
public void RequestPlayerCounts_AggregatesMapsAndWorld_IgnoresUnreported()
{
var (p1a, _) = AddPlayer("a", 1);
var (p1b, _) = AddPlayer("b", 1);
var (pw, _) = AddPlayer("w", VTRSyncConstants.WorldMapId);
var (pn, _) = AddPlayer("n", -1, hasReportedCurrentMap: false);

var reqConn = new RecordingConnection("req");
var reqPlayer = new ServerPlayer(999, reqConn)
{
currentMapId = 1,
hasReportedCurrentMap = true,
};
reqConn.serverPlayer = reqPlayer;
reqConn.ChangeState(ConnectionStateEnum.ServerPlaying);
server.playerManager.Players.Add(reqPlayer);

PlayingState(reqPlayer).HandleRequestPlayerCounts(new ClientRequestPlayerCountsPacket());

Assert.That(reqConn.SentPackets, Does.Contain(Packets.Server_PlayerCounts));
}

[Test]
public void RequestPlayerCounts_DoesNotBroadcastToOtherPlayers()
{
var (other, otherConn) = AddPlayer("other", 1);
var (reqPlayer, _) = AddPlayer("req", 1);

PlayingState(reqPlayer).HandleRequestPlayerCounts(new ClientRequestPlayerCountsPacket());

Assert.That(otherConn.SentPackets, Does.Not.Contain(Packets.Server_PlayerCounts));
}
}

internal static class VTRSyncConstants
{
public const int WorldMapId = -2;
}
Loading