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 sourceLevels = new Dictionary(); private readonly Dictionary sinkLevels = new Dictionary(); private readonly Dictionary checkpoints = new Dictionary(StringComparer.OrdinalIgnoreCase); private SweBoundaryManifest boundaryManifest; private HashSet activeSourceIds = new HashSet(); private HashSet activeSinkIds = new HashSet(); private UdpClient commandSocket; private IPEndPoint commandEndpoint; private IPEndPoint questEndpoint; private readonly Dictionary frameSubscribers = new Dictionary(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(); } 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(); 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(); activeSinkIds = new HashSet(); 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(); 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(); 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 CloneDictionary(Dictionary source) { var clone = new Dictionary(); 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 sourceLevels; public Dictionary sinkLevels; } } }