Files
DTrierFlood_New/Assets/Scripts/Networking/Server/SweServerRuntime.cs

1009 lines
37 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using FloodSWE.IO;
using FloodSWE.Streaming;
using FloodSWE.TileGraph;
using UnityEngine;
namespace FloodSWE.Networking
{
public sealed class SweServerRuntime : MonoBehaviour
{
[Header("Simulation")]
public SweTileSimulator simulator;
public SweTileLoader tileLoader;
public string activeLod = "lod1";
public int activeTileX = 1;
public int activeTileY = 1;
public bool applyTileOnStart = true;
[Header("Boundary Manifest")]
public TextAsset boundaryManifestAsset;
public string boundaryManifestPath = "GeoData/export_swe/swe_boundaries.json";
[Header("Network")]
public int commandListenPort = 29010;
public string questHost = "";
public int questFramePort = 29011;
public bool acceptFirstCommandSenderAsQuest = true;
[Header("Frame Stream")]
public float targetFrameHz = 10.0f;
[Tooltip("Maximum UDP payload bytes for a single frame datagram.")]
public int maxFramePayloadBytes = 60000;
[Tooltip("If enabled, oversized frames are downsampled until they fit max payload size.")]
public bool downsampleOversizedFrames = true;
[Tooltip("Lowest allowed resolution when downsampling oversized frames.")]
public int minFrameResolution = 64;
[Header("Source/Sink Control")]
public float sourceDepthRatePerLevelMps = 0.01f;
public float sinkDepthRatePerLevelMps = 0.01f;
[Tooltip("Applied to free_outflow sinks when no explicit sink level is set.")]
public float freeOutflowBaseLevel = 1.0f;
public bool applyFreeOutflowByDefault = true;
[Tooltip("Applies an initial source level on startup for quick validation without client commands.")]
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>();
private readonly Dictionary<string, SweCheckpointState> checkpoints = new Dictionary<string, SweCheckpointState>(StringComparer.OrdinalIgnoreCase);
private SweBoundaryManifest boundaryManifest;
private HashSet<int> activeSourceIds = new HashSet<int>();
private HashSet<int> activeSinkIds = new HashSet<int>();
private UdpClient commandSocket;
private IPEndPoint commandEndpoint;
private IPEndPoint questEndpoint;
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>();
}
if (simulator == null)
{
Debug.LogError("SweServerRuntime: missing SweTileSimulator reference.");
enabled = false;
return;
}
if (applyTileOnStart)
{
bool loaded = TrySetActiveTile(activeLod, activeTileX, activeTileY);
if (!loaded)
{
Debug.LogWarning($"SweServerRuntime: failed to load tile {activeLod} ({activeTileX},{activeTileY}).");
}
}
simulator.emitHeightmapPackets = true;
simulator.packetEveryNTicks = ComputePacketInterval(simulator.tickSeconds, targetFrameHz);
simulator.HeightmapPacketReady += OnHeightmapPacket;
LoadBoundaryManifest();
RefreshActiveTileBoundaryIds();
ApplyDefaultSourceIfConfigured();
RecomputeExternalDepthRate();
StartNetworking();
}
private void OnDisable()
{
if (simulator != null)
{
simulator.HeightmapPacketReady -= OnHeightmapPacket;
}
StopNetworking();
}
private void Update()
{
PollCommands();
UpdateThroughputSmoothing();
}
private void OnHeightmapPacket(HeightmapPacket packet)
{
TrackSimulationProgress(packet);
if (frameSubscribers.Count == 0)
{
return;
}
if (!TryBuildTransmitPayload(packet, out HeightmapPacket transmitPacket, out byte[] payload))
{
return;
}
var stale = new List<string>();
foreach (var kv in frameSubscribers)
{
try
{
commandSocket.Send(payload, payload.Length, kv.Value);
transmittedFramePackets++;
transmittedFrameBytes += payload.Length;
lastFramePayloadBytes = payload.Length;
}
catch (Exception ex)
{
stale.Add(kv.Key);
Debug.LogWarning($"SweServerRuntime: failed to send frame to {kv.Value}. {ex.Message}");
}
}
for (int i = 0; i < stale.Count; i++)
{
if (frameSubscribers.Remove(stale[i]))
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
}
}
private bool TryBuildTransmitPayload(HeightmapPacket originalPacket, out HeightmapPacket transmitPacket, out byte[] payload)
{
transmitPacket = originalPacket;
payload = SweUdpProtocol.EncodeFrame(transmitPacket);
int maxBytes = Mathf.Clamp(maxFramePayloadBytes, 1200, 65507);
if (payload.Length <= maxBytes)
{
return true;
}
if (!downsampleOversizedFrames)
{
WarnOversizedFrame(originalPacket.FrameId, originalPacket.Resolution, originalPacket.Resolution, payload.Length, maxBytes);
droppedFramePackets++;
return false;
}
if (!originalPacket.TryDecodeHeights(out float[] sourceHeights))
{
if (lastOversizeWarningFrameId != originalPacket.FrameId)
{
lastOversizeWarningFrameId = originalPacket.FrameId;
Debug.LogWarning($"SweServerRuntime: oversized frame {originalPacket.FrameId} ({payload.Length} bytes) and decode failed.");
}
droppedFramePackets++;
return false;
}
int sourceRes = originalPacket.Resolution;
int targetRes = sourceRes;
int minRes = Mathf.Max(8, minFrameResolution);
while (payload.Length > maxBytes && targetRes > minRes)
{
targetRes = Mathf.Max(minRes, targetRes / 2);
float[] downsampled = DownsampleHeightsNearest(sourceHeights, sourceRes, targetRes);
transmitPacket = HeightmapPacket.FromHeights(originalPacket.FrameId, originalPacket.Tile, targetRes, downsampled);
payload = SweUdpProtocol.EncodeFrame(transmitPacket);
}
if (payload.Length <= maxBytes)
{
return true;
}
WarnOversizedFrame(originalPacket.FrameId, sourceRes, targetRes, payload.Length, maxBytes);
droppedFramePackets++;
return false;
}
private float[] DownsampleHeightsNearest(float[] source, int sourceRes, int targetRes)
{
float[] result = new float[targetRes * targetRes];
float denom = Mathf.Max(1.0f, targetRes - 1.0f);
float sourceMax = sourceRes - 1.0f;
int idx = 0;
for (int y = 0; y < targetRes; y++)
{
int sy = Mathf.Clamp(Mathf.RoundToInt((y / denom) * sourceMax), 0, sourceRes - 1);
int sourceRow = sy * sourceRes;
for (int x = 0; x < targetRes; x++)
{
int sx = Mathf.Clamp(Mathf.RoundToInt((x / denom) * sourceMax), 0, sourceRes - 1);
result[idx++] = source[sourceRow + sx];
}
}
return result;
}
private void WarnOversizedFrame(int frameId, int sourceRes, int finalRes, int payloadBytes, int maxBytes)
{
if (lastOversizeWarningFrameId == frameId)
{
return;
}
lastOversizeWarningFrameId = frameId;
Debug.LogWarning(
$"SweServerRuntime: dropped frame {frameId}; payload {payloadBytes}B exceeds max {maxBytes}B " +
$"(sourceRes={sourceRes}, finalRes={finalRes}).");
}
private void StartNetworking()
{
try
{
commandSocket = new UdpClient(commandListenPort);
commandSocket.Client.Blocking = false;
commandEndpoint = new IPEndPoint(IPAddress.Any, 0);
}
catch (Exception ex)
{
Debug.LogError($"SweServerRuntime: failed to start command socket on port {commandListenPort}. {ex.Message}");
enabled = false;
return;
}
if (!string.IsNullOrWhiteSpace(questHost))
{
try
{
IPAddress[] addresses = Dns.GetHostAddresses(questHost.Trim());
if (addresses.Length > 0)
{
questEndpoint = new IPEndPoint(addresses[0], questFramePort);
AddFrameSubscriber(questEndpoint);
Debug.Log($"SweServerRuntime: fixed Quest endpoint {questEndpoint}.");
}
}
catch (Exception ex)
{
Debug.LogWarning($"SweServerRuntime: could not resolve quest host '{questHost}'. {ex.Message}");
}
}
}
private void StopNetworking()
{
if (commandSocket != null)
{
commandSocket.Close();
commandSocket = null;
}
commandEndpoint = null;
questEndpoint = null;
if (frameSubscribers.Count > 0)
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
frameSubscribers.Clear();
}
private void PollCommands()
{
if (commandSocket == null)
{
return;
}
while (commandSocket.Available > 0)
{
byte[] raw;
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
try
{
raw = commandSocket.Receive(ref sender);
}
catch (SocketException)
{
break;
}
if (raw == null || raw.Length == 0)
{
continue;
}
receivedControlPackets++;
if (acceptFirstCommandSenderAsQuest && questEndpoint == null)
{
questEndpoint = new IPEndPoint(sender.Address, questFramePort);
AddFrameSubscriber(questEndpoint);
Debug.Log($"SweServerRuntime: learned Quest endpoint {questEndpoint} from command sender {sender}.");
}
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);
}
}
private void HandleCommand(SweControlCommand command, IPEndPoint sender)
{
string cmd = command.command != null ? command.command.Trim().ToLowerInvariant() : string.Empty;
switch (cmd)
{
case "set_source_level":
sourceLevels[command.sourceId] = command.sourceLevel;
RecomputeExternalDepthRate();
SendAck(sender, $"source {command.sourceId} level={command.sourceLevel:F3}");
break;
case "set_sink_level":
sinkLevels[command.sinkId] = command.sinkLevel;
RecomputeExternalDepthRate();
SendAck(sender, $"sink {command.sinkId} level={command.sinkLevel:F3}");
break;
case "set_active_tile":
if (TrySetActiveTile(command.lod, command.tileX, command.tileY))
{
SendAck(sender, $"active tile: {activeLod} ({activeTileX},{activeTileY})");
}
else
{
SendAck(sender, $"active tile missing: {command.lod} ({command.tileX},{command.tileY})");
}
break;
case "save_checkpoint":
SaveCheckpoint(command.checkpoint);
SendAck(sender, $"checkpoint saved: {command.checkpoint}");
break;
case "reset_checkpoint":
bool restored = RestoreCheckpoint(command.checkpoint);
SendAck(sender, restored ? $"checkpoint restored: {command.checkpoint}" : $"checkpoint missing: {command.checkpoint}");
break;
case "apply_porosity_stamp":
bool stamped = simulator.ApplyPorosityStampNormalized(new Vector2(command.u, command.v), command.radius, command.porosity);
SendAck(sender, stamped ? "porosity stamp applied" : "porosity stamp failed");
break;
case "hello":
if (acceptFirstCommandSenderAsQuest)
{
var updated = new IPEndPoint(sender.Address, questFramePort);
if (questEndpoint == null || !questEndpoint.Equals(updated))
{
questEndpoint = updated;
Debug.Log($"SweServerRuntime: refreshed Quest endpoint to {questEndpoint} from hello.");
}
AddFrameSubscriber(updated);
}
SendAck(sender, "hello");
break;
case "disconnect":
RemoveFrameSubscriber(sender.Address, questFramePort);
SendAck(sender, "disconnected");
break;
default:
SendAck(sender, $"unknown command: {command.command}");
break;
}
}
private bool TrySetActiveTile(string lod, int tileX, int tileY)
{
string targetLod = string.IsNullOrWhiteSpace(lod) ? activeLod : lod.Trim();
int targetX = tileX;
int targetY = tileY;
if (tileLoader == null)
{
activeLod = targetLod;
activeTileX = targetX;
activeTileY = targetY;
simulator.packetTileId = new TileId(SweBoundaryManifest.ParseLod(activeLod), activeTileX, activeTileY);
RefreshActiveTileBoundaryIds();
RecomputeExternalDepthRate();
return true;
}
bool loaded = tileLoader.ApplyToSimulator(targetLod, targetX, targetY, simulator);
if (!loaded)
{
if (verboseDiagnostics && tileLoader != null)
{
Debug.LogWarning($"SweServerRuntime: tile load diagnostics => {tileLoader.LastLoadSummary}");
}
return false;
}
activeLod = targetLod;
activeTileX = targetX;
activeTileY = targetY;
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;
}
private void SendAck(IPEndPoint target, string message)
{
if (commandSocket == null || target == null)
{
return;
}
byte[] payload = SweUdpProtocol.EncodeAck(message);
try
{
commandSocket.Send(payload, payload.Length, target);
ackPacketsSent++;
}
catch (Exception ex)
{
Debug.LogWarning($"SweServerRuntime: failed to send ack. {ex.Message}");
}
}
private void LoadBoundaryManifest()
{
string json = null;
if (boundaryManifestAsset != null)
{
json = boundaryManifestAsset.text;
}
else if (!string.IsNullOrWhiteSpace(boundaryManifestPath))
{
string path = boundaryManifestPath;
if (!Path.IsPathRooted(path))
{
path = Path.Combine(Directory.GetCurrentDirectory(), boundaryManifestPath);
}
if (File.Exists(path))
{
json = File.ReadAllText(path);
}
}
if (!SweBoundaryManifest.TryLoad(json, out boundaryManifest))
{
Debug.LogWarning("SweServerRuntime: boundary manifest missing or invalid. Source/sink control disabled for this run.");
boundaryManifest = null;
lastForcingStatus = "disabled:manifest_missing_or_invalid";
}
}
private void RefreshActiveTileBoundaryIds()
{
activeSourceIds = new HashSet<int>();
activeSinkIds = new HashSet<int>();
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;
}
if (tile.source_ids != null)
{
for (int i = 0; i < tile.source_ids.Length; i++)
{
SweBoundaryTileIdRef entry = tile.source_ids[i];
if (entry != null && entry.id > 0)
{
activeSourceIds.Add(entry.id);
}
}
}
if (tile.sink_ids != null)
{
for (int i = 0; i < tile.sink_ids.Length; i++)
{
SweBoundaryTileIdRef entry = tile.sink_ids[i];
if (entry != null && entry.id > 0)
{
activeSinkIds.Add(entry.id);
}
}
}
Debug.Log($"SweServerRuntime: active tile sources={activeSourceIds.Count}, sinks={activeSinkIds.Count}");
}
private void RecomputeExternalDepthRate()
{
var sourceRates = new Dictionary<int, float>();
foreach (int sourceId in activeSourceIds)
{
if (!sourceLevels.TryGetValue(sourceId, out float value))
{
continue;
}
float rate = Mathf.Max(0.0f, value) * sourceDepthRatePerLevelMps;
if (rate > 0.0f)
{
sourceRates[sourceId] = rate;
}
}
var sinkRates = new Dictionary<int, float>();
foreach (int sinkId in activeSinkIds)
{
if (sinkLevels.TryGetValue(sinkId, out float value))
{
float rate = Mathf.Max(0.0f, value) * sinkDepthRatePerLevelMps;
if (rate > 0.0f)
{
sinkRates[sinkId] = rate;
}
continue;
}
if (applyFreeOutflowByDefault &&
boundaryManifest != null &&
boundaryManifest.TryGetSink(sinkId, out SweBoundarySink sink) &&
sink != null &&
sink.@params != null &&
sink.@params.IsMode("free_outflow"))
{
float rate = Mathf.Max(0.0f, freeOutflowBaseLevel) * sinkDepthRatePerLevelMps;
if (rate > 0.0f)
{
sinkRates[sinkId] = rate;
}
}
}
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;
}
if (simulator.TryGetBoundaryIdMasks(out int[] sourceMask, out int[] sinkMask))
{
int n = simulator.gridRes * simulator.gridRes;
float[] perCell = new float[n];
bool anyNonZero = false;
int forcedCellCount = 0;
for (int i = 0; i < n; i++)
{
if (!IsBorderCell(i, simulator.gridRes))
{
perCell[i] = 0.0f;
continue;
}
float rate = 0.0f;
if (sourceMask != null)
{
int sourceId = sourceMask[i];
if (sourceId > 0 && sourceRates.TryGetValue(sourceId, out float sourceRate))
{
rate += sourceRate;
}
}
if (sinkMask != null)
{
int sinkId = sinkMask[i];
if (sinkId > 0 && sinkRates.TryGetValue(sinkId, out float sinkRate))
{
rate -= sinkRate;
}
}
perCell[i] = rate;
anyNonZero = anyNonZero || Mathf.Abs(rate) > 1e-9f;
if (Mathf.Abs(rate) > 1e-9f)
{
forcedCellCount++;
}
}
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
{
string key = $"{activeLod}|{activeTileX}|{activeTileY}";
if (!string.Equals(lastMissingMaskWarningTileKey, key, StringComparison.Ordinal))
{
lastMissingMaskWarningTileKey = key;
Debug.LogWarning(
$"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.
simulator.ClearExternalDepthRateMap();
simulator.SetExternalDepthRate(0.0f);
}
private static bool IsBorderCell(int flatIndex, int resolution)
{
if (resolution <= 1)
{
return true;
}
int x = flatIndex % resolution;
int y = flatIndex / resolution;
return x == 0 || y == 0 || x == resolution - 1 || y == resolution - 1;
}
private void ApplyDefaultSourceIfConfigured()
{
if (!applyDefaultSourceOnStart)
{
return;
}
if (defaultSourceId <= 0)
{
Debug.LogWarning("SweServerRuntime: default source is enabled, but defaultSourceId is <= 0.");
return;
}
if (!activeSourceIds.Contains(defaultSourceId))
{
Debug.LogWarning(
$"SweServerRuntime: default source id {defaultSourceId} is not active on tile {activeLod} ({activeTileX},{activeTileY}).");
return;
}
float clamped = Mathf.Max(0.0f, defaultSourceLevel);
sourceLevels[defaultSourceId] = clamped;
Debug.Log($"SweServerRuntime: applied startup source id={defaultSourceId} level={clamped:0.###}");
}
private void SaveCheckpoint(string checkpointName)
{
if (string.IsNullOrWhiteSpace(checkpointName))
{
checkpointName = "default";
}
if (!simulator.TryCaptureWaterHeights(out float[] water))
{
Debug.LogWarning("SweServerRuntime: checkpoint capture failed (water).");
return;
}
simulator.TryCapturePorosity(out float[] porosity);
SweCheckpointState state = new SweCheckpointState
{
waterHeights = water,
porosityValues = porosity,
sourceLevels = CloneDictionary(sourceLevels),
sinkLevels = CloneDictionary(sinkLevels),
};
checkpoints[checkpointName] = state;
}
private bool RestoreCheckpoint(string checkpointName)
{
if (string.IsNullOrWhiteSpace(checkpointName))
{
checkpointName = "default";
}
if (!checkpoints.TryGetValue(checkpointName, out SweCheckpointState state) || state == null)
{
return false;
}
bool restoredWater = simulator.TryRestoreWaterHeights(state.waterHeights, true);
if (!restoredWater)
{
return false;
}
if (state.porosityValues != null)
{
simulator.TryRestorePorosity(state.porosityValues);
}
sourceLevels.Clear();
sinkLevels.Clear();
if (state.sourceLevels != null)
{
foreach (var pair in state.sourceLevels)
{
sourceLevels[pair.Key] = pair.Value;
}
}
if (state.sinkLevels != null)
{
foreach (var pair in state.sinkLevels)
{
sinkLevels[pair.Key] = pair.Value;
}
}
RecomputeExternalDepthRate();
return true;
}
private static Dictionary<int, float> CloneDictionary(Dictionary<int, float> source)
{
var clone = new Dictionary<int, float>();
if (source == null)
{
return clone;
}
foreach (var pair in source)
{
clone[pair.Key] = pair.Value;
}
return clone;
}
private static int ComputePacketInterval(float tickSeconds, float hz)
{
if (hz <= 0.0f || tickSeconds <= 0.0f)
{
return 1;
}
float targetSeconds = 1.0f / hz;
return Mathf.Max(1, Mathf.RoundToInt(targetSeconds / tickSeconds));
}
private void AddFrameSubscriber(IPEndPoint endpoint)
{
if (endpoint == null)
{
return;
}
frameSubscribers[MakeSubscriberKey(endpoint)] = endpoint;
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
private void RemoveFrameSubscriber(IPAddress address, int port)
{
if (address == null)
{
return;
}
string key = MakeSubscriberKey(address, port);
if (frameSubscribers.Remove(key))
{
lastClientChangeTimeUnscaled = Time.unscaledTime;
}
if (questEndpoint != null &&
questEndpoint.Address.Equals(address) &&
questEndpoint.Port == port)
{
questEndpoint = null;
}
}
private static string MakeSubscriberKey(IPEndPoint endpoint)
{
return MakeSubscriberKey(endpoint.Address, endpoint.Port);
}
private static string MakeSubscriberKey(IPAddress address, int port)
{
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
{
public float[] waterHeights;
public float[] porosityValues;
public Dictionary<int, float> sourceLevels;
public Dictionary<int, float> sinkLevels;
}
}
}