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

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;
namespace FloodSWE.IO
@@ -24,30 +25,85 @@ namespace FloodSWE.IO
[Header("Runtime")]
public bool cacheTextures = true;
public bool verboseDiagnostics = true;
public bool logMissingTextures = true;
private readonly Dictionary<TileKey, TileRecord> records = new Dictionary<TileKey, TileRecord>();
private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> missingTextureWarnings = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool indexLoaded;
private int loadAttempts;
private int loadSuccesses;
private string lastRequestedTileKey = "n/a";
private string lastLoadedTileKey = "n/a";
private string lastLoadSummary = "No tile load attempted yet.";
private bool lastLoadSucceeded;
private int indexRecordCount;
private int indexSkippedRows;
public string LastRequestedTileKey => lastRequestedTileKey;
public string LastLoadedTileKey => lastLoadedTileKey;
public string LastLoadSummary => lastLoadSummary;
public bool LastLoadSucceeded => lastLoadSucceeded;
public int IndexedTileCount => indexRecordCount;
public int IndexSkippedRows => indexSkippedRows;
public int LoadAttempts => loadAttempts;
public int LoadSuccesses => loadSuccesses;
public bool TryLoadTile(string lod, int tileX, int tileY, out SweTileData data)
{
data = default;
loadAttempts++;
lastRequestedTileKey = MakeTileKeyLabel(lod, tileX, tileY);
EnsureIndexLoaded();
TileKey key = new TileKey(lod, tileX, tileY);
if (!records.TryGetValue(key, out TileRecord record))
{
lastLoadSucceeded = false;
lastLoadSummary = $"tile={lastRequestedTileKey}; status=missing_in_index; indexed={records.Count}";
Debug.LogWarning($"SweTileLoader: {lastLoadSummary}");
return false;
}
Texture2D height = LoadTexture(record.HeightPath);
Texture2D porosity = LoadTexture(record.PorosityPath);
Texture2D buildings = LoadTexture(record.BuildingPath);
Texture2D sourceIds = LoadTexture(BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "source_ids"));
Texture2D sinkIds = LoadTexture(BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "sink_ids"));
string sourcePath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "source_ids");
string sinkPath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "sink_ids");
Texture2D sourceIds = LoadTexture(sourcePath);
Texture2D sinkIds = LoadTexture(sinkPath);
data = new SweTileData(record, height, porosity, buildings, sourceIds, sinkIds);
return height != null || porosity != null || buildings != null || sourceIds != null || sinkIds != null;
bool loadedAny = height != null || porosity != null || buildings != null || sourceIds != null || sinkIds != null;
lastLoadSucceeded = loadedAny;
if (loadedAny)
{
loadSuccesses++;
lastLoadedTileKey = lastRequestedTileKey;
}
lastLoadSummary = BuildLoadSummary(
lastRequestedTileKey,
record,
height,
porosity,
buildings,
sourceIds,
sinkIds,
sourcePath,
sinkPath);
if (!loadedAny)
{
Debug.LogWarning($"SweTileLoader: {lastLoadSummary}");
}
else if (verboseDiagnostics)
{
Debug.Log($"SweTileLoader: {lastLoadSummary}");
}
return loadedAny;
}
public bool ApplyToSimulator(string lod, int tileX, int tileY, SweTileSimulator simulator)
@@ -74,6 +130,21 @@ namespace FloodSWE.IO
simulator.sourceIdMask = data.SourceIds;
simulator.sinkIdMask = data.SinkIds;
if (verboseDiagnostics)
{
Debug.Log(
$"SweTileLoader: applied tile {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
$"to simulator (height={DescribeTexture(data.Height)}, porosity={DescribeTexture(data.Porosity)}, " +
$"sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)}).");
}
if (data.SourceIds == null || data.SinkIds == null)
{
Debug.LogWarning(
$"SweTileLoader: boundary mask coverage is incomplete for {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
$"(sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)}).");
}
return true;
}
@@ -90,6 +161,8 @@ namespace FloodSWE.IO
}
records.Clear();
indexRecordCount = 0;
indexSkippedRows = 0;
string text = tileIndexCsv != null ? tileIndexCsv.text : LoadTextFromPath();
if (string.IsNullOrWhiteSpace(text))
@@ -124,16 +197,24 @@ namespace FloodSWE.IO
if (parts.Length < headerColumns)
{
indexSkippedRows++;
continue;
}
if (parts.Length <= 11)
{
indexSkippedRows++;
continue;
}
string lod = parts[0].Trim();
if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileX))
{
indexSkippedRows++;
continue;
}
if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileY))
{
indexSkippedRows++;
continue;
}
@@ -148,7 +229,14 @@ namespace FloodSWE.IO
);
}
indexRecordCount = records.Count;
indexLoaded = true;
if (verboseDiagnostics)
{
Debug.Log(
$"SweTileLoader: tile index loaded (records={indexRecordCount}, skipped={indexSkippedRows}, " +
$"source={DescribeIndexSource()}).");
}
}
private string LoadTextFromPath()
@@ -201,7 +289,12 @@ namespace FloodSWE.IO
switch (sourceMode)
{
case SourceMode.Resources:
texture = Resources.Load<Texture2D>(ToResourcesPath(path));
string resourcePath = ToResourcesPath(path);
texture = Resources.Load<Texture2D>(resourcePath);
if (texture == null)
{
LogMissingTextureOnce(path, $"Resources/{resourcePath}");
}
break;
case SourceMode.StreamingAssets:
case SourceMode.AbsolutePath:
@@ -279,7 +372,7 @@ namespace FloodSWE.IO
}
if (!File.Exists(resolved))
{
Debug.LogWarning($"SweTileLoader: texture not found {resolved}");
LogMissingTextureOnce(path, resolved);
return null;
}
@@ -298,6 +391,83 @@ namespace FloodSWE.IO
return texture;
}
private string BuildLoadSummary(
string requestedTile,
in TileRecord record,
Texture2D height,
Texture2D porosity,
Texture2D buildings,
Texture2D sourceIds,
Texture2D sinkIds,
string sourcePath,
string sinkPath)
{
var sb = new StringBuilder(256);
sb.Append("tile=").Append(requestedTile)
.Append("; index=").Append(indexRecordCount)
.Append("; height=").Append(DescribeTexture(height))
.Append("; porosity=").Append(DescribeTexture(porosity))
.Append("; buildings=").Append(DescribeTexture(buildings))
.Append("; sourceMask=").Append(DescribeTexture(sourceIds))
.Append(" (").Append(sourcePath).Append(")")
.Append("; sinkMask=").Append(DescribeTexture(sinkIds))
.Append(" (").Append(sinkPath).Append(")")
.Append("; loads=").Append(loadSuccesses).Append("/").Append(loadAttempts);
return sb.ToString();
}
private void LogMissingTextureOnce(string requestedPath, string resolvedPath)
{
if (!logMissingTextures)
{
return;
}
if (!missingTextureWarnings.Add(resolvedPath ?? requestedPath ?? "unknown"))
{
return;
}
Debug.LogWarning(
$"SweTileLoader: texture not found (requested='{requestedPath}', resolved='{resolvedPath}', mode={sourceMode}).");
}
private string DescribeIndexSource()
{
if (tileIndexCsv != null)
{
return $"TextAsset:{tileIndexCsv.name}";
}
if (sourceMode == SourceMode.StreamingAssets)
{
return $"StreamingAssets/{tileIndexPath}";
}
if (sourceMode == SourceMode.AbsolutePath)
{
return tileIndexPath;
}
return $"Resources/{ToResourcesPath(tileIndexPath)}";
}
private static string MakeTileKeyLabel(string lod, int x, int y)
{
string normalized = string.IsNullOrWhiteSpace(lod) ? "lod?" : lod.Trim();
return $"{normalized} ({x},{y})";
}
private static string DescribeTexture(Texture2D texture)
{
if (texture == null)
{
return "missing";
}
return $"{texture.width}x{texture.height},readable={texture.isReadable}";
}
private readonly struct TileKey : IEquatable<TileKey>
{
public readonly string Lod;

View File

@@ -0,0 +1,140 @@
using TMPro;
using UnityEngine;
namespace FloodSWE.Networking
{
/// <summary>
/// Binds SweServerRuntime diagnostics to user-provided TMP text fields.
/// </summary>
public sealed class SweServerDiagnosticsHud : MonoBehaviour
{
[Header("References")]
[SerializeField] private SweServerRuntime serverRuntime;
[SerializeField] private TMP_Text simulationText;
[SerializeField] private TMP_Text connectionText;
[SerializeField] private TMP_Text commandText;
[SerializeField] private TMP_Text forcingText;
[SerializeField] private TMP_Text tileLoadText;
[Header("Behavior")]
[SerializeField] private bool autoFindRuntime = true;
[SerializeField] private float refreshIntervalSeconds = 0.15f;
[Header("Labels")]
[SerializeField] private string connectedLabel = "Connected";
[SerializeField] private string staleLabel = "Stale";
[SerializeField] private string disconnectedLabel = "Disconnected";
[SerializeField] private string unknownAgeLabel = "n/a";
[SerializeField] private string numberFormat = "0.00";
[Header("Colors")]
[SerializeField] private Color connectedColor = new Color(0.4f, 1.0f, 0.4f);
[SerializeField] private Color staleColor = new Color(1.0f, 0.9f, 0.3f);
[SerializeField] private Color disconnectedColor = new Color(1.0f, 0.5f, 0.5f);
private float nextRefreshTime;
private void OnEnable()
{
if (serverRuntime == null && autoFindRuntime)
{
serverRuntime = FindFirstObjectByType<SweServerRuntime>();
}
Refresh(true);
}
private void Update()
{
if (Time.unscaledTime < nextRefreshTime)
{
return;
}
Refresh(false);
}
private void Refresh(bool immediate)
{
nextRefreshTime = Time.unscaledTime + Mathf.Max(0.02f, refreshIntervalSeconds);
if (serverRuntime == null)
{
SetText(simulationText, "Sim Speed: n/a");
SetText(connectionText, "Connection: runtime missing");
SetText(commandText, "Last Command: n/a");
SetText(forcingText, "Forcing: n/a");
SetText(tileLoadText, "Tile Load: runtime missing");
if (connectionText != null)
{
connectionText.color = disconnectedColor;
}
return;
}
string simLine = $"Sim Speed: {serverRuntime.SimulatedSecondsPerSecond.ToString(numberFormat)} sim-s/s";
if (!serverRuntime.HasSimulationThroughput)
{
simLine = "Sim Speed: warming up";
}
SetText(simulationText, simLine);
string state;
Color stateColor;
if (!serverRuntime.HasConnectedClients)
{
state = disconnectedLabel;
stateColor = disconnectedColor;
}
else if (serverRuntime.HasRecentClientSignal)
{
state = connectedLabel;
stateColor = connectedColor;
}
else
{
state = staleLabel;
stateColor = staleColor;
}
string connectionLine =
$"Connection: {state} | clients={serverRuntime.ConnectedClientCount} | endpoint={serverRuntime.QuestEndpointLabel}";
SetText(connectionText, connectionLine);
if (connectionText != null)
{
connectionText.color = stateColor;
}
string age = float.IsInfinity(serverRuntime.LastCommandAgeSeconds)
? unknownAgeLabel
: $"{serverRuntime.LastCommandAgeSeconds.ToString(numberFormat)}s";
string commandLine =
$"Last Command: {serverRuntime.LastCommandName} from {serverRuntime.LastCommandSender} ({age}) | " +
$"ctrl={serverRuntime.DecodedControlPackets}/{serverRuntime.ReceivedControlPackets} invalid={serverRuntime.InvalidControlPackets} ack={serverRuntime.AckPacketsSent}";
SetText(commandText, commandLine);
string forcingLine =
$"Forcing: {serverRuntime.LastForcingStatus} | cells={serverRuntime.LastForcedCellCount} | tile={serverRuntime.ActiveTileLabel}";
SetText(forcingText, forcingLine);
string tileLine =
$"Tile Load: {serverRuntime.TileLoadSummary} | framePayload={serverRuntime.LastFramePayloadBytes}B " +
$"frames={serverRuntime.TransmittedFramePackets} dropped={serverRuntime.DroppedFramePackets}";
SetText(tileLoadText, tileLine);
if (immediate)
{
nextRefreshTime = Time.unscaledTime;
}
}
private static void SetText(TMP_Text target, string value)
{
if (target == null || target.text == value)
{
return;
}
target.text = value;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d968f37c7f174572b92aa1b0f2de2ce7

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
{