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 boundaryProfiles = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary checkpoints = new Dictionary(StringComparer.OrdinalIgnoreCase); private SweBoundaryManifest boundaryManifest; private HashSet activeSourceIds = new HashSet(); private HashSet activeSinkIds = new HashSet(); private readonly Dictionary activeBoundaryInflowGhostCells = new Dictionary(); private readonly Dictionary activeBoundarySinkGhostCells = new Dictionary(); private readonly Dictionary activeSourceAreaCells = new Dictionary(); private readonly Dictionary activeSinkCells = new Dictionary(); private bool activeTileHasBoundaryCellGroups; private UdpClient commandSocket; private IPEndPoint commandEndpoint; private IPEndPoint questEndpoint; private readonly Dictionary frameSubscribers = new Dictionary(StringComparer.Ordinal); private readonly Dictionary stateSubscribers = 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(); stateSubscribers.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": ApplyLegacySourceLevel(command.sourceId, command.sourceLevel); SendAck(sender, $"legacy source {command.sourceId} level={command.sourceLevel:F3}"); break; case "set_sink_level": ApplyLegacySinkLevel(command.sinkId, command.sinkLevel); SendAck(sender, $"legacy sink {command.sinkId} level={command.sinkLevel:F3}"); break; case "set_boundary_profile": if (TryApplyBoundaryProfileCommand(command, out string profileResult)) { SendAck(sender, profileResult); BroadcastBoundaryState("update"); } else { SendAck(sender, "set_boundary_profile rejected"); } break; case "set_boundaries_bulk": if (TryApplyBoundaryBulkCommand(command, out string bulkResult)) { SendAck(sender, bulkResult); BroadcastBoundaryState("update"); } else { SendAck(sender, "set_boundaries_bulk rejected"); } break; case "get_boundary_config": SendBoundaryState(sender, "config"); SendAck(sender, "boundary_config_sent"); break; case "subscribe_boundary_updates": if (command.subscribe) { AddStateSubscriber(sender); SendBoundaryState(sender, "config"); SendAck(sender, "boundary_updates_subscribed"); } else { RemoveStateSubscriber(sender); SendAck(sender, "boundary_updates_unsubscribed"); } 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); RemoveStateSubscriber(sender); 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(); BroadcastBoundaryState("tile_changed"); 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(); BroadcastBoundaryState("tile_changed"); 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"; boundaryProfiles.Clear(); return; } InitializeBoundaryProfilesFromManifest(); } private void InitializeBoundaryProfilesFromManifest() { boundaryProfiles.Clear(); if (boundaryManifest == null) { return; } if (boundaryManifest.boundaries != null) { for (int i = 0; i < boundaryManifest.boundaries.Length; i++) { SweBoundaryDefinition boundary = boundaryManifest.boundaries[i]; if (boundary == null || boundary.id <= 0) { continue; } SweBoundaryProfile profile = boundary.default_state != null ? boundary.default_state.ToProfile(boundary.kind, boundary.id) : new SweBoundaryProfile { boundaryKind = NormalizeBoundaryKind(boundary.kind), boundaryId = boundary.id, enabled = false, waterLevelM = 0.0f, velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = 0.0f, }; SetBoundaryProfileInternal(profile); } } if (boundaryManifest.sources != null) { for (int i = 0; i < boundaryManifest.sources.Length; i++) { SweBoundarySource source = boundaryManifest.sources[i]; if (source == null || source.id <= 0) { continue; } string key = MakeBoundaryProfileKey("boundary_inflow", source.id); if (!boundaryProfiles.ContainsKey(key)) { SetBoundaryProfileInternal(new SweBoundaryProfile { boundaryKind = "boundary_inflow", boundaryId = source.id, enabled = false, waterLevelM = 0.0f, velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = 0.0f, }); } } } if (boundaryManifest.sinks != null) { for (int i = 0; i < boundaryManifest.sinks.Length; i++) { SweBoundarySink sink = boundaryManifest.sinks[i]; if (sink == null || sink.id <= 0) { continue; } string key = MakeBoundaryProfileKey("sink", sink.id); if (!boundaryProfiles.ContainsKey(key)) { bool freeOutflow = sink.@params != null && sink.@params.IsMode("free_outflow"); SetBoundaryProfileInternal(new SweBoundaryProfile { boundaryKind = "sink", boundaryId = sink.id, enabled = freeOutflow, waterLevelM = 0.0f, velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = 0.0f, }); } } } } private void RefreshActiveTileBoundaryIds() { activeSourceIds = new HashSet(); activeSinkIds = new HashSet(); activeBoundaryInflowGhostCells.Clear(); activeBoundarySinkGhostCells.Clear(); activeSourceAreaCells.Clear(); activeSinkCells.Clear(); activeTileHasBoundaryCellGroups = false; 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; } CollectIdsFromRefs(tile.source_ids, activeSourceIds); CollectIdsFromRefs(tile.sink_ids, activeSinkIds); CollectIdsFromRefs(tile.boundary_inflow_ids, activeSourceIds); CollectCellGroups(tile.boundary_cells, activeBoundaryInflowGhostCells, activeSourceIds); CollectCellGroups(tile.boundary_sink_cells, activeBoundarySinkGhostCells, activeSinkIds); CollectCellGroups(tile.source_area_cells, activeSourceAreaCells, activeSourceIds); CollectCellGroups(tile.sink_cells, activeSinkCells, activeSinkIds); activeTileHasBoundaryCellGroups = activeBoundaryInflowGhostCells.Count > 0 || activeBoundarySinkGhostCells.Count > 0 || activeSourceAreaCells.Count > 0 || activeSinkCells.Count > 0; Debug.Log( $"SweServerRuntime: active tile inflowIds={activeSourceIds.Count}, sinkIds={activeSinkIds.Count}, " + $"boundaryInGhostGroups={activeBoundaryInflowGhostCells.Count}, boundaryOutGhostGroups={activeBoundarySinkGhostCells.Count}, " + $"sourceAreaGroups={activeSourceAreaCells.Count}, sinkGroups={activeSinkCells.Count}"); } private void RecomputeExternalDepthRate() { if (simulator == null) { return; } if (activeTileHasBoundaryCellGroups) { RecomputeBoundaryProfilesWithCellGroups(); return; } RecomputeLegacyBoundaryDepthRate(); } private void RecomputeBoundaryProfilesWithCellGroups() { var ghostIndices = new List(128); var ghostLevels = new List(128); var ghostVelocities = new List(128); var ghostInflowCells = new HashSet(); var ghostOutflowCells = new HashSet(); var suppressedOutflowCells = new HashSet(); var boundarySinkIdsWithGhostOutflow = new HashSet(); foreach (var pair in activeBoundaryInflowGhostCells) { int boundaryId = pair.Key; if (!TryResolveBoundaryProfile("boundary_inflow", boundaryId, out SweBoundaryProfile profile) || profile == null) { continue; } if (!profile.enabled) { continue; } int[] cells = pair.Value; if (cells == null || cells.Length == 0) { continue; } float level = Mathf.Max(0.0f, profile.waterLevelM); Vector2 velocity = new Vector2(profile.velocityUMps, profile.velocityVMps); for (int i = 0; i < cells.Length; i++) { int idx = cells[i]; if (idx < 0) { continue; } ghostIndices.Add(idx); ghostLevels.Add(level); ghostVelocities.Add(velocity); ghostInflowCells.Add(idx); } } bool hasGhostForcing = ghostIndices.Count > 0 && simulator.SetGhostBoundaryOverrides(ghostIndices.ToArray(), ghostLevels.ToArray(), ghostVelocities.ToArray()); if (!hasGhostForcing) { simulator.ClearGhostBoundaryOverrides(); } foreach (var pair in activeBoundarySinkGhostCells) { int boundaryId = pair.Key; if (boundaryId <= 0) { continue; } if (!TryResolveBoundaryProfile("sink", boundaryId, out SweBoundaryProfile sinkProfile) || sinkProfile == null || !sinkProfile.enabled) { continue; } boundarySinkIdsWithGhostOutflow.Add(boundaryId); int[] cells = pair.Value; if (cells == null || cells.Length == 0) { continue; } for (int i = 0; i < cells.Length; i++) { int idx = cells[i]; if (idx < 0) { continue; } if (ghostInflowCells.Contains(idx)) { suppressedOutflowCells.Add(idx); continue; } ghostOutflowCells.Add(idx); } } bool hasGhostOutflow = ghostOutflowCells.Count > 0 && simulator.SetGhostFreeOutflowCells(new List(ghostOutflowCells).ToArray()); if (!hasGhostOutflow) { simulator.ClearGhostFreeOutflowCells(); } int totalCells = simulator.gridRes * simulator.gridRes; float[] perCell = new float[totalCells]; bool hasDepthForcing = false; int forcedDepthCells = 0; foreach (var pair in activeSourceAreaCells) { int boundaryId = pair.Key; if (!TryResolveBoundaryProfile("source_area", boundaryId, out SweBoundaryProfile profile) || profile == null) { continue; } if (!profile.enabled) { continue; } float rate = profile.depthRateMps; if (Mathf.Abs(rate) <= 1e-9f && profile.waterLevelM > 0.0f) { rate = profile.waterLevelM * sourceDepthRatePerLevelMps; } if (rate <= 0.0f) { continue; } int[] cells = pair.Value; if (cells == null) { continue; } for (int i = 0; i < cells.Length; i++) { int idx = cells[i]; if (idx < 0 || idx >= totalCells) { continue; } perCell[idx] += rate; } } foreach (var pair in activeSinkCells) { int boundaryId = pair.Key; if (boundarySinkIdsWithGhostOutflow.Contains(boundaryId)) { continue; } float rate = 0.0f; if (TryResolveBoundaryProfile("sink", boundaryId, out SweBoundaryProfile profile) && profile != null && profile.enabled) { rate = profile.depthRateMps; if (Mathf.Abs(rate) <= 1e-9f && profile.waterLevelM > 0.0f) { rate = -profile.waterLevelM * sinkDepthRatePerLevelMps; } } if (Mathf.Abs(rate) <= 1e-9f && applyFreeOutflowByDefault && IsFreeOutflowSink(boundaryId)) { rate = -Mathf.Max(0.0f, freeOutflowBaseLevel) * sinkDepthRatePerLevelMps; } if (rate > 0.0f) { rate = -rate; } if (Mathf.Abs(rate) <= 1e-9f) { continue; } int[] cells = pair.Value; if (cells == null) { continue; } for (int i = 0; i < cells.Length; i++) { int idx = cells[i]; if (idx < 0 || idx >= totalCells) { continue; } perCell[idx] += rate; } } for (int i = 0; i < perCell.Length; i++) { if (Mathf.Abs(perCell[i]) > 1e-9f) { hasDepthForcing = true; forcedDepthCells++; } } if (hasDepthForcing && simulator.SetExternalDepthRateMap(perCell)) { simulator.SetExternalDepthRate(0.0f); } else { simulator.ClearExternalDepthRateMap(); simulator.SetExternalDepthRate(0.0f); hasDepthForcing = false; forcedDepthCells = 0; } if (!hasGhostForcing && !hasDepthForcing && !hasGhostOutflow) { lastForcedCellCount = 0; lastForcingStatus = "disabled:no_active_boundary_profiles"; return; } lastForcedCellCount = ghostIndices.Count + forcedDepthCells + (hasGhostOutflow ? ghostOutflowCells.Count : 0); if (hasGhostForcing && hasDepthForcing && hasGhostOutflow) { lastForcingStatus = "ghost+localized+free_outflow"; } else if (hasGhostForcing && hasDepthForcing) { lastForcingStatus = "ghost+localized"; } else if (hasGhostForcing && hasGhostOutflow) { lastForcingStatus = "ghost+free_outflow"; } else if (hasDepthForcing && hasGhostOutflow) { lastForcingStatus = "localized+free_outflow"; } else if (hasGhostForcing) { lastForcingStatus = "ghost_only"; } else if (hasGhostOutflow) { lastForcingStatus = "free_outflow_only"; } else { lastForcingStatus = "localized"; } if (verboseDiagnostics) { Debug.Log( $"SweServerRuntime: forcing applied status={lastForcingStatus} ghost={ghostIndices.Count} depth={forcedDepthCells} freeOutflow={ghostOutflowCells.Count} suppressedOutflowByInflow={suppressedOutflowCells.Count}"); } } private void RecomputeLegacyBoundaryDepthRate() { simulator.ClearGhostBoundaryOverrides(); simulator.ClearGhostFreeOutflowCells(); 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_legacy"; 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 = "legacy_localized"; Debug.Log($"SweServerRuntime: applied localized boundary forcing cells={forcedCellCount}"); return; } lastForcedCellCount = 0; lastForcingStatus = "disabled:boundary_masks_zero_or_apply_failed_legacy"; } 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_legacy"; } // Strict mode: no masks means no forcing. This prevents non-physical uniform flooding. simulator.ClearExternalDepthRateMap(); simulator.SetExternalDepthRate(0.0f); } private bool TryResolveBoundaryProfile(string kind, int boundaryId, out SweBoundaryProfile profile) { profile = null; if (boundaryId <= 0) { return false; } string normalizedKind = NormalizeBoundaryKind(kind); string key = MakeBoundaryProfileKey(normalizedKind, boundaryId); if (boundaryProfiles.TryGetValue(key, out profile) && profile != null) { return true; } if (boundaryManifest != null && boundaryManifest.TryGetBoundary(normalizedKind, boundaryId, out SweBoundaryDefinition boundary) && boundary != null) { profile = boundary.default_state != null ? boundary.default_state.ToProfile(normalizedKind, boundaryId) : new SweBoundaryProfile { boundaryKind = normalizedKind, boundaryId = boundaryId, enabled = false, waterLevelM = 0.0f, velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = 0.0f, }; SetBoundaryProfileInternal(profile); return true; } return false; } private bool IsFreeOutflowSink(int sinkId) { if (boundaryManifest == null || sinkId <= 0) { return false; } if (boundaryManifest.TryGetBoundary("sink", sinkId, out SweBoundaryDefinition boundary) && boundary != null && boundary.@params != null && boundary.@params.IsMode("free_outflow")) { return true; } return boundaryManifest.TryGetSink(sinkId, out SweBoundarySink sink) && sink != null && sink.@params != null && sink.@params.IsMode("free_outflow"); } 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 static void CollectIdsFromRefs(SweBoundaryTileIdRef[] refs, HashSet outIds) { if (refs == null || outIds == null) { return; } for (int i = 0; i < refs.Length; i++) { SweBoundaryTileIdRef entry = refs[i]; if (entry != null && entry.id > 0) { outIds.Add(entry.id); } } } private static void CollectCellGroups( SweBoundaryTileCellGroup[] groups, Dictionary outGroups, HashSet outIds) { if (groups == null || outGroups == null || outIds == null) { return; } for (int i = 0; i < groups.Length; i++) { SweBoundaryTileCellGroup group = groups[i]; if (group == null || group.id <= 0 || group.cells == null || group.cells.Length == 0) { continue; } outIds.Add(group.id); outGroups[group.id] = (int[])group.cells.Clone(); } } private void ApplyLegacySourceLevel(int sourceId, float sourceLevel) { if (sourceId <= 0) { return; } sourceLevels[sourceId] = sourceLevel; var profile = new SweBoundaryProfile { boundaryKind = "boundary_inflow", boundaryId = sourceId, enabled = sourceLevel > 0.0f, waterLevelM = Mathf.Max(0.0f, sourceLevel), velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = 0.0f, }; SetBoundaryProfileInternal(profile); RecomputeExternalDepthRate(); } private void ApplyLegacySinkLevel(int sinkId, float sinkLevel) { if (sinkId <= 0) { return; } sinkLevels[sinkId] = sinkLevel; var profile = new SweBoundaryProfile { boundaryKind = "sink", boundaryId = sinkId, enabled = sinkLevel > 0.0f, waterLevelM = Mathf.Max(0.0f, sinkLevel), velocityUMps = 0.0f, velocityVMps = 0.0f, depthRateMps = -Mathf.Max(0.0f, sinkLevel) * sinkDepthRatePerLevelMps, }; SetBoundaryProfileInternal(profile); RecomputeExternalDepthRate(); } private bool TryApplyBoundaryProfileCommand(SweControlCommand command, out string result) { result = "invalid_boundary_profile"; if (command == null || command.boundaryId <= 0) { return false; } string kind = NormalizeBoundaryKind(command.boundaryKind); var profile = new SweBoundaryProfile { boundaryKind = kind, boundaryId = command.boundaryId, enabled = command.enabled != 0, waterLevelM = Mathf.Max(0.0f, command.waterLevelM), velocityUMps = command.velocityUMps, velocityVMps = command.velocityVMps, depthRateMps = command.depthRateMps, }; if (kind == "sink" && profile.depthRateMps > 0.0f) { profile.depthRateMps = -profile.depthRateMps; } SetBoundaryProfileInternal(profile); RecomputeExternalDepthRate(); result = $"boundary profile set: {kind}:{profile.boundaryId}"; return true; } private bool TryApplyBoundaryBulkCommand(SweControlCommand command, out string result) { result = "invalid_boundary_bulk"; if (command == null || command.boundaries == null || command.boundaries.Length == 0) { return false; } if (command.replaceAll) { InitializeBoundaryProfilesFromManifest(); } int applied = 0; for (int i = 0; i < command.boundaries.Length; i++) { SweBoundaryProfile incoming = command.boundaries[i]; if (incoming == null || incoming.boundaryId <= 0) { continue; } string kind = NormalizeBoundaryKind(incoming.boundaryKind); var profile = incoming.Clone(); profile.boundaryKind = kind; if (kind == "sink" && profile.depthRateMps > 0.0f) { profile.depthRateMps = -profile.depthRateMps; } SetBoundaryProfileInternal(profile); applied++; } if (applied <= 0) { return false; } RecomputeExternalDepthRate(); result = $"boundary bulk applied: {applied}"; return true; } private void AddStateSubscriber(IPEndPoint endpoint) { if (endpoint == null) { return; } stateSubscribers[MakeSubscriberKey(endpoint)] = endpoint; } private void RemoveStateSubscriber(IPEndPoint endpoint) { if (endpoint == null) { return; } stateSubscribers.Remove(MakeSubscriberKey(endpoint)); } private void SendBoundaryState(IPEndPoint target, string messageType) { if (commandSocket == null || target == null) { return; } SweBoundaryStateMessage state = BuildBoundaryStateMessage(messageType); byte[] payload = SweUdpProtocol.EncodeState(state); try { commandSocket.Send(payload, payload.Length, target); } catch (Exception ex) { Debug.LogWarning($"SweServerRuntime: failed to send boundary state to {target}. {ex.Message}"); } } private void BroadcastBoundaryState(string messageType) { if (commandSocket == null || stateSubscribers.Count == 0) { return; } SweBoundaryStateMessage state = BuildBoundaryStateMessage(messageType); byte[] payload = SweUdpProtocol.EncodeState(state); var stale = new List(); foreach (var pair in stateSubscribers) { try { commandSocket.Send(payload, payload.Length, pair.Value); } catch { stale.Add(pair.Key); } } for (int i = 0; i < stale.Count; i++) { stateSubscribers.Remove(stale[i]); } } private SweBoundaryStateMessage BuildBoundaryStateMessage(string messageType) { var profiles = new List(boundaryProfiles.Values); profiles.Sort((a, b) => { int kind = string.Compare( a != null ? a.boundaryKind : string.Empty, b != null ? b.boundaryKind : string.Empty, StringComparison.OrdinalIgnoreCase); if (kind != 0) { return kind; } int aid = a != null ? a.boundaryId : 0; int bid = b != null ? b.boundaryId : 0; return aid.CompareTo(bid); }); var snapshot = new SweBoundaryProfile[profiles.Count]; for (int i = 0; i < profiles.Count; i++) { snapshot[i] = profiles[i] != null ? profiles[i].Clone() : null; } return new SweBoundaryStateMessage { messageType = messageType, schemaVersion = boundaryManifest != null ? boundaryManifest.schema_version : 0, lod = activeLod, tileX = activeTileX, tileY = activeTileY, boundaries = snapshot, }; } private static string NormalizeBoundaryKind(string kind) { if (string.IsNullOrWhiteSpace(kind)) { return "boundary_inflow"; } string normalized = kind.Trim().ToLowerInvariant(); if (normalized == "source" || normalized == "boundary_source") { return "boundary_inflow"; } if (normalized == "sourcearea" || normalized == "source-area") { return "source_area"; } return normalized; } private static string MakeBoundaryProfileKey(string kind, int id) { return $"{NormalizeBoundaryKind(kind)}:{id}"; } private void SetBoundaryProfileInternal(SweBoundaryProfile profile) { if (profile == null || profile.boundaryId <= 0) { return; } string kind = NormalizeBoundaryKind(profile.boundaryKind); profile.boundaryKind = kind; string key = MakeBoundaryProfileKey(kind, profile.boundaryId); boundaryProfiles[key] = profile.Clone(); } 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); ApplyLegacySourceLevel(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), boundaryProfiles = CloneBoundaryProfiles(boundaryProfiles), }; 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(); boundaryProfiles.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; } } if (state.boundaryProfiles != null) { for (int i = 0; i < state.boundaryProfiles.Length; i++) { SweBoundaryProfile profile = state.boundaryProfiles[i]; if (profile == null) { continue; } SetBoundaryProfileInternal(profile); } } RecomputeExternalDepthRate(); BroadcastBoundaryState("checkpoint_restored"); 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 SweBoundaryProfile[] CloneBoundaryProfiles(Dictionary source) { if (source == null || source.Count == 0) { return Array.Empty(); } var outProfiles = new SweBoundaryProfile[source.Count]; int idx = 0; foreach (var pair in source) { outProfiles[idx++] = pair.Value != null ? pair.Value.Clone() : null; } return outProfiles; } 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; public SweBoundaryProfile[] boundaryProfiles; } } }