Add bindable SWE server diagnostics and TMP HUD adapter

This commit is contained in:
2026-02-10 21:15:17 +01:00
parent 1ec973c48e
commit 662278858b
4 changed files with 519 additions and 7 deletions

View File

@@ -49,6 +49,11 @@ namespace FloodSWE.Networking
public bool applyDefaultSourceOnStart = false;
public int defaultSourceId = 1;
public float defaultSourceLevel = 1.0f;
public bool verboseDiagnostics = true;
public float simThroughputAveragingSeconds = 1.0f;
[Range(0.0f, 1.0f)]
public float simThroughputSmoothing = 0.35f;
public float clientSignalTimeoutSeconds = 3.0f;
private readonly Dictionary<int, float> sourceLevels = new Dictionary<int, float>();
private readonly Dictionary<int, float> sinkLevels = new Dictionary<int, float>();
@@ -64,9 +69,53 @@ namespace FloodSWE.Networking
private readonly Dictionary<string, IPEndPoint> frameSubscribers = new Dictionary<string, IPEndPoint>(StringComparer.Ordinal);
private int lastOversizeWarningFrameId = -1;
private string lastMissingMaskWarningTileKey = "";
private int receivedControlPackets;
private int decodedControlPackets;
private int invalidControlPackets;
private int ackPacketsSent;
private int droppedFramePackets;
private int transmittedFramePackets;
private long transmittedFrameBytes;
private int lastFramePayloadBytes;
private string lastCommandName = "none";
private string lastCommandSender = "n/a";
private float lastCommandTimeUnscaled = -1.0f;
private float lastClientChangeTimeUnscaled = -1.0f;
private float simulatedSecondsPerSecond;
private int lastPacketFrameId = -1;
private float lastPacketTimeUnscaled = -1.0f;
private float throughputWindowRealSeconds;
private float throughputWindowSimSeconds;
private int lastForcedCellCount;
private string lastForcingStatus = "idle";
public string ActiveTileLabel => $"{activeLod} ({activeTileX},{activeTileY})";
public int ConnectedClientCount => frameSubscribers.Count;
public bool HasConnectedClients => frameSubscribers.Count > 0;
public string QuestEndpointLabel => questEndpoint != null ? questEndpoint.ToString() : "none";
public float SimulatedSecondsPerSecond => simulatedSecondsPerSecond;
public bool HasSimulationThroughput => simulatedSecondsPerSecond > 0.0f;
public string LastCommandName => lastCommandName;
public string LastCommandSender => lastCommandSender;
public float LastCommandAgeSeconds => lastCommandTimeUnscaled >= 0.0f ? Time.unscaledTime - lastCommandTimeUnscaled : float.PositiveInfinity;
public bool HasRecentClientSignal => lastCommandTimeUnscaled >= 0.0f && LastCommandAgeSeconds <= Mathf.Max(0.1f, clientSignalTimeoutSeconds);
public int ReceivedControlPackets => receivedControlPackets;
public int DecodedControlPackets => decodedControlPackets;
public int InvalidControlPackets => invalidControlPackets;
public int AckPacketsSent => ackPacketsSent;
public int DroppedFramePackets => droppedFramePackets;
public int TransmittedFramePackets => transmittedFramePackets;
public long TransmittedFrameBytes => transmittedFrameBytes;
public int LastFramePayloadBytes => lastFramePayloadBytes;
public string LastForcingStatus => lastForcingStatus;
public int LastForcedCellCount => lastForcedCellCount;
public string TileLoadSummary => tileLoader != null ? tileLoader.LastLoadSummary : "SweTileLoader missing";
public float LastClientChangeAgeSeconds => lastClientChangeTimeUnscaled >= 0.0f ? Time.unscaledTime - lastClientChangeTimeUnscaled : float.PositiveInfinity;
public string DiagnosticsSummary => BuildDiagnosticsSummary();
private void OnEnable()
{
ResetDiagnostics();
if (simulator == null)
{
simulator = GetComponent<SweTileSimulator>();
@@ -112,10 +161,12 @@ namespace FloodSWE.Networking
private void Update()
{
PollCommands();
UpdateThroughputSmoothing();
}
private void OnHeightmapPacket(HeightmapPacket packet)
{
TrackSimulationProgress(packet);
if (frameSubscribers.Count == 0)
{
return;
@@ -132,6 +183,9 @@ namespace FloodSWE.Networking
try
{
commandSocket.Send(payload, payload.Length, kv.Value);
transmittedFramePackets++;
transmittedFrameBytes += payload.Length;
lastFramePayloadBytes = payload.Length;
}
catch (Exception ex)
{
@@ -142,7 +196,10 @@ namespace FloodSWE.Networking
for (int i = 0; i < stale.Count; i++)
{
frameSubscribers.Remove(stale[i]);
if (frameSubscribers.Remove(stale[i]))
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
}
}
@@ -159,6 +216,7 @@ namespace FloodSWE.Networking
if (!downsampleOversizedFrames)
{
WarnOversizedFrame(originalPacket.FrameId, originalPacket.Resolution, originalPacket.Resolution, payload.Length, maxBytes);
droppedFramePackets++;
return false;
}
@@ -169,6 +227,7 @@ namespace FloodSWE.Networking
lastOversizeWarningFrameId = originalPacket.FrameId;
Debug.LogWarning($"SweServerRuntime: oversized frame {originalPacket.FrameId} ({payload.Length} bytes) and decode failed.");
}
droppedFramePackets++;
return false;
}
@@ -190,6 +249,7 @@ namespace FloodSWE.Networking
}
WarnOversizedFrame(originalPacket.FrameId, sourceRes, targetRes, payload.Length, maxBytes);
droppedFramePackets++;
return false;
}
@@ -270,6 +330,10 @@ namespace FloodSWE.Networking
commandEndpoint = null;
questEndpoint = null;
if (frameSubscribers.Count > 0)
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
frameSubscribers.Clear();
}
@@ -297,6 +361,7 @@ namespace FloodSWE.Networking
{
continue;
}
receivedControlPackets++;
if (acceptFirstCommandSenderAsQuest && questEndpoint == null)
{
@@ -307,9 +372,14 @@ namespace FloodSWE.Networking
if (!SweUdpProtocol.TryDecodeControl(raw, out SweControlCommand command) || command == null)
{
invalidControlPackets++;
continue;
}
decodedControlPackets++;
lastCommandName = string.IsNullOrWhiteSpace(command.command) ? "unknown" : command.command.Trim();
lastCommandSender = sender.ToString();
lastCommandTimeUnscaled = Time.unscaledTime;
HandleCommand(command, sender);
}
}
@@ -403,6 +473,10 @@ namespace FloodSWE.Networking
bool loaded = tileLoader.ApplyToSimulator(targetLod, targetX, targetY, simulator);
if (!loaded)
{
if (verboseDiagnostics && tileLoader != null)
{
Debug.LogWarning($"SweServerRuntime: tile load diagnostics => {tileLoader.LastLoadSummary}");
}
return false;
}
@@ -412,6 +486,10 @@ namespace FloodSWE.Networking
simulator.packetTileId = new TileId(SweBoundaryManifest.ParseLod(activeLod), activeTileX, activeTileY);
RefreshActiveTileBoundaryIds();
RecomputeExternalDepthRate();
if (verboseDiagnostics && tileLoader != null)
{
Debug.Log($"SweServerRuntime: active tile set => {tileLoader.LastLoadSummary}");
}
return true;
}
@@ -426,6 +504,7 @@ namespace FloodSWE.Networking
try
{
commandSocket.Send(payload, payload.Length, target);
ackPacketsSent++;
}
catch (Exception ex)
{
@@ -458,6 +537,7 @@ namespace FloodSWE.Networking
{
Debug.LogWarning("SweServerRuntime: boundary manifest missing or invalid. Source/sink control disabled for this run.");
boundaryManifest = null;
lastForcingStatus = "disabled:manifest_missing_or_invalid";
}
}
@@ -468,12 +548,14 @@ namespace FloodSWE.Networking
if (boundaryManifest == null)
{
lastForcingStatus = "disabled:no_manifest";
return;
}
if (!boundaryManifest.TryGetTile(activeLod, activeTileX, activeTileY, out SweBoundaryTile tile) || tile == null)
{
Debug.LogWarning($"SweServerRuntime: no boundary entry for tile {activeLod} ({activeTileX},{activeTileY}).");
lastForcingStatus = "disabled:no_tile_boundary_entry";
return;
}
@@ -552,6 +634,8 @@ namespace FloodSWE.Networking
bool hasBoundaryRates = sourceRates.Count > 0 || sinkRates.Count > 0;
if (!hasBoundaryRates)
{
lastForcedCellCount = 0;
lastForcingStatus = "disabled:no_active_source_or_sink_levels";
simulator.ClearExternalDepthRateMap();
simulator.SetExternalDepthRate(0.0f);
return;
@@ -602,9 +686,14 @@ namespace FloodSWE.Networking
if (anyNonZero && simulator.SetExternalDepthRateMap(perCell))
{
simulator.SetExternalDepthRate(0.0f);
lastForcedCellCount = forcedCellCount;
lastForcingStatus = "localized";
Debug.Log($"SweServerRuntime: applied localized boundary forcing cells={forcedCellCount}");
return;
}
lastForcedCellCount = 0;
lastForcingStatus = "disabled:boundary_masks_zero_or_apply_failed";
}
else
{
@@ -616,6 +705,9 @@ namespace FloodSWE.Networking
$"SweServerRuntime: no readable source/sink ID masks for tile {activeLod} ({activeTileX},{activeTileY}); " +
"disabling source/sink forcing.");
}
lastForcedCellCount = 0;
lastForcingStatus = "disabled:masks_unavailable";
}
// Strict mode: no masks means no forcing. This prevents non-physical uniform flooding.
@@ -767,6 +859,7 @@ namespace FloodSWE.Networking
}
frameSubscribers[MakeSubscriberKey(endpoint)] = endpoint;
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
private void RemoveFrameSubscriber(IPAddress address, int port)
@@ -777,7 +870,10 @@ namespace FloodSWE.Networking
}
string key = MakeSubscriberKey(address, port);
frameSubscribers.Remove(key);
if (frameSubscribers.Remove(key))
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
if (questEndpoint != null &&
questEndpoint.Address.Equals(address) &&
questEndpoint.Port == port)
@@ -796,6 +892,110 @@ namespace FloodSWE.Networking
return $"{address}:{port}";
}
private void TrackSimulationProgress(HeightmapPacket packet)
{
if (simulator == null || packet == null)
{
return;
}
float now = Time.unscaledTime;
if (lastPacketFrameId < 0 || lastPacketTimeUnscaled < 0.0f)
{
lastPacketFrameId = packet.FrameId;
lastPacketTimeUnscaled = now;
return;
}
int frameDelta = packet.FrameId - lastPacketFrameId;
if (frameDelta <= 0)
{
lastPacketFrameId = packet.FrameId;
lastPacketTimeUnscaled = now;
return;
}
float realDelta = Mathf.Max(0.0001f, now - lastPacketTimeUnscaled);
float simDelta = frameDelta * Mathf.Max(0.0001f, simulator.tickSeconds);
throughputWindowRealSeconds += realDelta;
throughputWindowSimSeconds += simDelta;
lastPacketFrameId = packet.FrameId;
lastPacketTimeUnscaled = now;
float minWindow = Mathf.Max(0.1f, simThroughputAveragingSeconds);
if (throughputWindowRealSeconds < minWindow)
{
return;
}
float raw = throughputWindowSimSeconds / Mathf.Max(0.0001f, throughputWindowRealSeconds);
float blend = Mathf.Clamp01(simThroughputSmoothing);
if (simulatedSecondsPerSecond <= 0.0f)
{
simulatedSecondsPerSecond = raw;
}
else
{
simulatedSecondsPerSecond = Mathf.Lerp(simulatedSecondsPerSecond, raw, blend);
}
throughputWindowRealSeconds = 0.0f;
throughputWindowSimSeconds = 0.0f;
}
private void UpdateThroughputSmoothing()
{
if (simulatedSecondsPerSecond <= 0.0f || lastPacketTimeUnscaled < 0.0f)
{
return;
}
float idleAge = Time.unscaledTime - lastPacketTimeUnscaled;
if (idleAge <= Mathf.Max(0.1f, simThroughputAveragingSeconds) * 2.0f)
{
return;
}
simulatedSecondsPerSecond = Mathf.MoveTowards(simulatedSecondsPerSecond, 0.0f, Time.unscaledDeltaTime * 2.0f);
if (simulatedSecondsPerSecond < 0.001f)
{
simulatedSecondsPerSecond = 0.0f;
}
}
private string BuildDiagnosticsSummary()
{
return
$"tile={ActiveTileLabel}; clients={ConnectedClientCount}; recentSignal={HasRecentClientSignal}; " +
$"simSecPerSec={simulatedSecondsPerSecond:0.00}; forcing={lastForcingStatus}; forcedCells={lastForcedCellCount}; " +
$"cmd={lastCommandName}@{lastCommandSender}; ctl={decodedControlPackets}/{receivedControlPackets} ack={ackPacketsSent}; " +
$"frames={transmittedFramePackets} dropped={droppedFramePackets} bytes={transmittedFrameBytes}; " +
$"loader={TileLoadSummary}";
}
private void ResetDiagnostics()
{
receivedControlPackets = 0;
decodedControlPackets = 0;
invalidControlPackets = 0;
ackPacketsSent = 0;
droppedFramePackets = 0;
transmittedFramePackets = 0;
transmittedFrameBytes = 0;
lastFramePayloadBytes = 0;
lastCommandName = "none";
lastCommandSender = "n/a";
lastCommandTimeUnscaled = -1.0f;
lastClientChangeTimeUnscaled = -1.0f;
simulatedSecondsPerSecond = 0.0f;
lastPacketFrameId = -1;
lastPacketTimeUnscaled = -1.0f;
throughputWindowRealSeconds = 0.0f;
throughputWindowSimSeconds = 0.0f;
lastForcedCellCount = 0;
lastForcingStatus = "idle";
}
[Serializable]
private sealed class SweCheckpointState
{