(ent.Owner);
diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs
index 7596e65e94b..000cbc6a2e9 100644
--- a/Robust.Server/GameObjects/ServerEntityManager.cs
+++ b/Robust.Server/GameObjects/ServerEntityManager.cs
@@ -229,21 +229,6 @@ public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel target
private void HandleEntityNetworkMessage(MsgEntity message)
{
- var msgT = message.SourceTick;
- var cT = _gameTiming.CurTick;
-
- if (msgT <= cT)
- {
- if (msgT < cT && _logLateMsgs)
- {
- _netEntSawmill.Warning("Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}",
- (int) msgT.Value - (int) cT.Value, message.MsgChannel.UserName, msgT, cT);
- }
-
- DispatchEntityNetworkMessage(message);
- return;
- }
-
_queue.Add(message);
}
diff --git a/Robust.Server/GameStates/PvsData.cs b/Robust.Server/GameStates/PvsData.cs
index 9f222460b02..f6778464a93 100644
--- a/Robust.Server/GameStates/PvsData.cs
+++ b/Robust.Server/GameStates/PvsData.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
+using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -115,6 +117,16 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion
public GameState? State;
+ ///
+ /// The serialized object.
+ ///
+ public MemoryStream? StateStream;
+
+ ///
+ /// Whether we should force reliable sending of the .
+ ///
+ public bool ForceSendReliably { get; set; }
+
///
/// Clears all stored game state data. This should only be used after the game state has been serialized.
///
diff --git a/Robust.Server/GameStates/PvsSystem.Dirty.cs b/Robust.Server/GameStates/PvsSystem.Dirty.cs
index 64d6f1150ed..98bab6daf07 100644
--- a/Robust.Server/GameStates/PvsSystem.Dirty.cs
+++ b/Robust.Server/GameStates/PvsSystem.Dirty.cs
@@ -75,15 +75,15 @@ private bool TryGetDirtyEntities(GameTick tick, [NotNullWhen(true)] out HashSet<
return true;
}
- private void CleanupDirty(ICommonSession[] sessions)
+ private void CleanupDirty()
{
using var _ = Histogram.WithLabels("Clean Dirty").NewTimer();
if (!CullingEnabled)
{
_seenAllEnts.Clear();
- foreach (var player in sessions)
+ foreach (var player in _sessions)
{
- _seenAllEnts.Add(player);
+ _seenAllEnts.Add(player.Session);
}
}
diff --git a/Robust.Server/GameStates/PvsSystem.Leave.cs b/Robust.Server/GameStates/PvsSystem.Leave.cs
index cae9c9cf551..bfb0041e5f7 100644
--- a/Robust.Server/GameStates/PvsSystem.Leave.cs
+++ b/Robust.Server/GameStates/PvsSystem.Leave.cs
@@ -17,13 +17,12 @@ internal sealed partial class PvsSystem
{
private WaitHandle? _leaveTask;
- private void ProcessLeavePvs(ICommonSession[] sessions)
+ private void ProcessLeavePvs()
{
- if (!CullingEnabled || sessions.Length == 0)
+ if (!CullingEnabled || _sessions.Length == 0)
return;
DebugTools.AssertNull(_leaveTask);
- _leaveJob.Setup(sessions);
if (_async)
{
@@ -76,29 +75,19 @@ private record struct PvsLeaveJob(PvsSystem _pvs) : IParallelRobustJob
{
public int BatchSize => 2;
private PvsSystem _pvs = _pvs;
- public int Count => _sessions.Length;
- private PvsSession[] _sessions;
+ public int Count => _pvs._sessions.Length;
+
public void Execute(int index)
{
try
{
- _pvs.ProcessLeavePvs(_sessions[index]);
+ _pvs.ProcessLeavePvs(_pvs._sessions[index]);
}
catch (Exception e)
{
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
}
}
-
- public void Setup(ICommonSession[] sessions)
- {
- // Copy references to PvsSession, in case players disconnect while the job is running.
- Array.Resize(ref _sessions, sessions.Length);
- for (var i = 0; i < sessions.Length; i++)
- {
- _sessions[i] = _pvs.PlayerData[sessions[i]];
- }
- }
}
}
diff --git a/Robust.Server/GameStates/PvsSystem.Send.cs b/Robust.Server/GameStates/PvsSystem.Send.cs
new file mode 100644
index 00000000000..382c4a7bc57
--- /dev/null
+++ b/Robust.Server/GameStates/PvsSystem.Send.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Threading.Tasks;
+using Prometheus;
+using Robust.Shared.Log;
+using Robust.Shared.Network.Messages;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Robust.Server.GameStates;
+
+internal sealed partial class PvsSystem
+{
+ ///
+ /// Compress and send game states to connected clients.
+ ///
+ private void SendStates()
+ {
+ // TODO PVS make this async
+ // AFAICT ForEachAsync doesn't support using a threadlocal PvsThreadResources.
+ // Though if it is getting pooled, does it really matter?
+
+ // If this does get run async, then ProcessDisconnections() has to ensure that the job has finished before modifying
+ // the sessions array
+
+ using var _ = Histogram.WithLabels("Send States").NewTimer();
+ var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
+ Parallel.ForEach(_sessions, opts, _threadResourcesPool.Get, SendSessionState, _threadResourcesPool.Return);
+ }
+
+ private PvsThreadResources SendSessionState(PvsSession data, ParallelLoopState state, PvsThreadResources resource)
+ {
+ try
+ {
+ SendSessionState(data, resource.CompressionContext);
+ }
+ catch (Exception e)
+ {
+ Log.Log(LogLevel.Error, e, $"Caught exception while sending mail for {data.Session}.");
+ }
+
+ return resource;
+ }
+
+ private void SendSessionState(PvsSession data, ZStdCompressionContext ctx)
+ {
+ DebugTools.AssertEqual(data.State, null);
+
+ // PVS benchmarks use dummy sessions.
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (data.Session.Channel is not DummyChannel)
+ {
+ DebugTools.AssertNotEqual(data.StateStream, null);
+ var msg = new MsgState
+ {
+ StateStream = data.StateStream,
+ ForceSendReliably = data.ForceSendReliably,
+ CompressionContext = ctx
+ };
+
+ _netMan.ServerSendMessage(msg, data.Session.Channel);
+ if (msg.ShouldSendReliably())
+ {
+ data.RequestedFull = false;
+ data.LastReceivedAck = _gameTiming.CurTick;
+ lock (PendingAcks)
+ {
+ PendingAcks.Add(data.Session);
+ }
+ }
+ }
+ else
+ {
+ // Always "ack" dummy sessions.
+ data.LastReceivedAck = _gameTiming.CurTick;
+ data.RequestedFull = false;
+ lock (PendingAcks)
+ {
+ PendingAcks.Add(data.Session);
+ }
+ }
+
+ data.StateStream?.Dispose();
+ data.StateStream = null;
+ }
+}
diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs
new file mode 100644
index 00000000000..ff3fd0812fc
--- /dev/null
+++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Threading.Tasks;
+using Prometheus;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Player;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Robust.Server.GameStates;
+
+internal sealed partial class PvsSystem
+{
+ [Dependency] private readonly IRobustSerializer _serializer = default!;
+
+ ///
+ /// Get and serialize objects for each player. Compressing & sending the states is done later.
+ ///
+ private void SerializeStates()
+ {
+ using var _ = Histogram.WithLabels("Serialize States").NewTimer();
+ var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
+ _oldestAck = GameTick.MaxValue.Value;
+ Parallel.For(-1, _sessions.Length, opts, SerializeState);
+ }
+
+ ///
+ /// Get and serialize a for a single session (or the current replay).
+ ///
+ private void SerializeState(int i)
+ {
+ try
+ {
+ var guid = i >= 0 ? _sessions[i].Session.UserId.UserId : default;
+ ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
+
+ if (i >= 0)
+ SerializeSessionState(_sessions[i]);
+ else
+ _replay.Update();
+
+ ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
+ }
+ catch (Exception e) // Catch EVERY exception
+ {
+ var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
+ Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
+ }
+ }
+
+ ///
+ /// Get and serialize a for a single session.
+ ///
+ private void SerializeSessionState(PvsSession data)
+ {
+ ComputeSessionState(data);
+ InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
+ DebugTools.AssertEqual(data.StateStream, null);
+
+ // PVS benchmarks use dummy sessions.
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (data.Session.Channel is not DummyChannel)
+ {
+ data.StateStream = RobustMemoryManager.GetMemoryStream();
+ _serializer.SerializeDirect(data.StateStream, data.State);
+ }
+
+ data.ClearState();
+ }
+}
diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs
index 264cd7341cf..9e3a87dee40 100644
--- a/Robust.Server/GameStates/PvsSystem.Session.cs
+++ b/Robust.Server/GameStates/PvsSystem.Session.cs
@@ -7,8 +7,6 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
-using Robust.Shared.Network;
-using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -27,49 +25,6 @@ internal sealed partial class PvsSystem
private List _disconnected = new();
- private void SendStateUpdate(ICommonSession session, PvsThreadResources resources)
- {
- var data = GetOrNewPvsSession(session);
- ComputeSessionState(data);
-
- InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
-
- // actually send the state
- var msg = new MsgState
- {
- State = data.State,
- CompressionContext = resources.CompressionContext
- };
-
- // PVS benchmarks use dummy sessions.
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
- if (session.Channel is not DummyChannel)
- {
- _netMan.ServerSendMessage(msg, session.Channel);
- if (msg.ShouldSendReliably())
- {
- data.RequestedFull = false;
- data.LastReceivedAck = _gameTiming.CurTick;
- lock (PendingAcks)
- {
- PendingAcks.Add(session);
- }
- }
- }
- else
- {
- // Always "ack" dummy sessions.
- data.LastReceivedAck = _gameTiming.CurTick;
- data.RequestedFull = false;
- lock (PendingAcks)
- {
- PendingAcks.Add(session);
- }
- }
-
- data.ClearState();
- }
-
private PvsSession GetOrNewPvsSession(ICommonSession session)
{
if (!PlayerData.TryGetValue(session, out var pvsSession))
@@ -104,7 +59,7 @@ internal void ComputeSessionState(PvsSession session)
session.PlayerStates,
_deletedEntities);
- session.State.ForceSendReliably = session.RequestedFull
+ session.ForceSendReliably = session.RequestedFull
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
}
@@ -125,14 +80,17 @@ private void UpdateSession(PvsSession session)
// Update visibility masks & viewer positions
// TODO PVS do this before sending state.
// I,e, we already enumerate over all eyes when computing visible chunks.
- Span positions = stackalloc MapCoordinates[session.Viewers.Length];
+ Span<(MapCoordinates pos, float scale)> positions = stackalloc (MapCoordinates, float)[session.Viewers.Length];
int i = 0;
foreach (var viewer in session.Viewers)
{
if (viewer.Comp2 != null)
session.VisMask |= viewer.Comp2.VisibilityMask;
- positions[i++] = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
+ var mapCoordinates = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
+ mapCoordinates = mapCoordinates.Offset(viewer.Comp2?.Offset ?? Vector2.Zero);
+ var scale = MathF.Max((viewer.Comp2?.PvsScale ?? 1), 0.1f);
+ positions[i++] = (mapCoordinates, scale);
}
if (!CullingEnabled || session.DisableCulling)
@@ -157,7 +115,7 @@ private void UpdateSession(PvsSession session)
DebugTools.Assert(!chunk.UpdateQueued);
DebugTools.Assert(!chunk.Dirty);
- foreach (var pos in positions)
+ foreach (var (pos, scale) in positions)
{
if (pos.MapId != chunk.Position.MapId)
continue;
@@ -165,8 +123,9 @@ private void UpdateSession(PvsSession session)
dist = Math.Min(dist, (pos.Position - chunk.Position.Position).LengthSquared());
var relative = Vector2.Transform(pos.Position, chunk.InvWorldMatrix) - chunk.Centre;
+
relative = Vector2.Abs(relative);
- chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y));
+ chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y) / scale);
}
distances.Add(dist);
diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs
index b3a051c09b4..7a53079a01e 100644
--- a/Robust.Server/GameStates/PvsSystem.cs
+++ b/Robust.Server/GameStates/PvsSystem.cs
@@ -3,10 +3,8 @@
using System.Diagnostics;
using System.Linq;
using System.Numerics;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
-using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Prometheus;
using Robust.Server.Configuration;
@@ -16,9 +14,6 @@
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -99,6 +94,10 @@ internal sealed partial class PvsSystem : EntitySystem
///