Add bindable SWE server diagnostics and TMP HUD adapter
This commit is contained in:
@@ -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;
|
||||
|
||||
140
Assets/Scripts/Networking/Server/SweServerDiagnosticsHud.cs
Normal file
140
Assets/Scripts/Networking/Server/SweServerDiagnosticsHud.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d968f37c7f174572b92aa1b0f2de2ce7
|
||||
@@ -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