Add bindable SWE server diagnostics and TMP HUD adapter
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user