Rework SWE boundary control to ghost inflows and boundary profiles
This commit is contained in:
@@ -57,16 +57,23 @@ namespace FloodSWE.Networking
|
||||
|
||||
private readonly Dictionary<int, float> sourceLevels = new Dictionary<int, float>();
|
||||
private readonly Dictionary<int, float> sinkLevels = new Dictionary<int, float>();
|
||||
private readonly Dictionary<string, SweBoundaryProfile> boundaryProfiles =
|
||||
new Dictionary<string, SweBoundaryProfile>(StringComparer.OrdinalIgnoreCase);
|
||||
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 readonly Dictionary<int, int[]> activeBoundaryInflowGhostCells = new Dictionary<int, int[]>();
|
||||
private readonly Dictionary<int, int[]> activeSourceAreaCells = new Dictionary<int, int[]>();
|
||||
private readonly Dictionary<int, int[]> activeSinkCells = new Dictionary<int, int[]>();
|
||||
private bool activeTileHasBoundaryCellGroups;
|
||||
|
||||
private UdpClient commandSocket;
|
||||
private IPEndPoint commandEndpoint;
|
||||
private IPEndPoint questEndpoint;
|
||||
private readonly Dictionary<string, IPEndPoint> frameSubscribers = new Dictionary<string, IPEndPoint>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, IPEndPoint> stateSubscribers = new Dictionary<string, IPEndPoint>(StringComparer.Ordinal);
|
||||
private int lastOversizeWarningFrameId = -1;
|
||||
private string lastMissingMaskWarningTileKey = "";
|
||||
private int receivedControlPackets;
|
||||
@@ -335,6 +342,7 @@ namespace FloodSWE.Networking
|
||||
lastClientChangeTimeUnscaled = Time.unscaledTime;
|
||||
}
|
||||
frameSubscribers.Clear();
|
||||
stateSubscribers.Clear();
|
||||
}
|
||||
|
||||
private void PollCommands()
|
||||
@@ -390,15 +398,56 @@ namespace FloodSWE.Networking
|
||||
switch (cmd)
|
||||
{
|
||||
case "set_source_level":
|
||||
sourceLevels[command.sourceId] = command.sourceLevel;
|
||||
RecomputeExternalDepthRate();
|
||||
SendAck(sender, $"source {command.sourceId} level={command.sourceLevel:F3}");
|
||||
ApplyLegacySourceLevel(command.sourceId, command.sourceLevel);
|
||||
SendAck(sender, $"legacy 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}");
|
||||
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":
|
||||
@@ -444,6 +493,7 @@ namespace FloodSWE.Networking
|
||||
|
||||
case "disconnect":
|
||||
RemoveFrameSubscriber(sender.Address, questFramePort);
|
||||
RemoveStateSubscriber(sender);
|
||||
SendAck(sender, "disconnected");
|
||||
break;
|
||||
|
||||
@@ -467,6 +517,7 @@ namespace FloodSWE.Networking
|
||||
simulator.packetTileId = new TileId(SweBoundaryManifest.ParseLod(activeLod), activeTileX, activeTileY);
|
||||
RefreshActiveTileBoundaryIds();
|
||||
RecomputeExternalDepthRate();
|
||||
BroadcastBoundaryState("tile_changed");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -486,6 +537,7 @@ namespace FloodSWE.Networking
|
||||
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}");
|
||||
@@ -538,6 +590,100 @@ namespace FloodSWE.Networking
|
||||
Debug.LogWarning("SweServerRuntime: boundary manifest missing or invalid. Source/sink control disabled for this run.");
|
||||
boundaryManifest = null;
|
||||
lastForcingStatus = "disabled:manifest_missing_or_invalid";
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +691,10 @@ namespace FloodSWE.Networking
|
||||
{
|
||||
activeSourceIds = new HashSet<int>();
|
||||
activeSinkIds = new HashSet<int>();
|
||||
activeBoundaryInflowGhostCells.Clear();
|
||||
activeSourceAreaCells.Clear();
|
||||
activeSinkCells.Clear();
|
||||
activeTileHasBoundaryCellGroups = false;
|
||||
|
||||
if (boundaryManifest == null)
|
||||
{
|
||||
@@ -559,35 +709,226 @@ namespace FloodSWE.Networking
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
CollectIdsFromRefs(tile.source_ids, activeSourceIds);
|
||||
CollectIdsFromRefs(tile.sink_ids, activeSinkIds);
|
||||
CollectIdsFromRefs(tile.boundary_inflow_ids, activeSourceIds);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
CollectCellGroups(tile.boundary_cells, activeBoundaryInflowGhostCells, activeSourceIds);
|
||||
CollectCellGroups(tile.source_area_cells, activeSourceAreaCells, activeSourceIds);
|
||||
CollectCellGroups(tile.sink_cells, activeSinkCells, activeSinkIds);
|
||||
|
||||
Debug.Log($"SweServerRuntime: active tile sources={activeSourceIds.Count}, sinks={activeSinkIds.Count}");
|
||||
activeTileHasBoundaryCellGroups =
|
||||
activeBoundaryInflowGhostCells.Count > 0 ||
|
||||
activeSourceAreaCells.Count > 0 ||
|
||||
activeSinkCells.Count > 0;
|
||||
|
||||
Debug.Log(
|
||||
$"SweServerRuntime: active tile inflowIds={activeSourceIds.Count}, sinkIds={activeSinkIds.Count}, " +
|
||||
$"boundaryGhostGroups={activeBoundaryInflowGhostCells.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<int>(128);
|
||||
var ghostLevels = new List<float>(128);
|
||||
var ghostVelocities = new List<Vector2>(128);
|
||||
|
||||
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++)
|
||||
{
|
||||
ghostIndices.Add(cells[i]);
|
||||
ghostLevels.Add(level);
|
||||
ghostVelocities.Add(velocity);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasGhostForcing =
|
||||
ghostIndices.Count > 0 &&
|
||||
simulator.SetGhostBoundaryOverrides(ghostIndices.ToArray(), ghostLevels.ToArray(), ghostVelocities.ToArray());
|
||||
if (!hasGhostForcing)
|
||||
{
|
||||
simulator.ClearGhostBoundaryOverrides();
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
lastForcedCellCount = 0;
|
||||
lastForcingStatus = "disabled:no_active_boundary_profiles";
|
||||
return;
|
||||
}
|
||||
|
||||
lastForcedCellCount = ghostIndices.Count + forcedDepthCells;
|
||||
if (hasGhostForcing && hasDepthForcing)
|
||||
{
|
||||
lastForcingStatus = "ghost+localized";
|
||||
}
|
||||
else if (hasGhostForcing)
|
||||
{
|
||||
lastForcingStatus = "ghost_only";
|
||||
}
|
||||
else
|
||||
{
|
||||
lastForcingStatus = "localized";
|
||||
}
|
||||
|
||||
if (verboseDiagnostics)
|
||||
{
|
||||
Debug.Log(
|
||||
$"SweServerRuntime: forcing applied status={lastForcingStatus} ghost={ghostIndices.Count} depth={forcedDepthCells}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RecomputeLegacyBoundaryDepthRate()
|
||||
{
|
||||
simulator.ClearGhostBoundaryOverrides();
|
||||
|
||||
var sourceRates = new Dictionary<int, float>();
|
||||
foreach (int sourceId in activeSourceIds)
|
||||
{
|
||||
@@ -635,7 +976,7 @@ namespace FloodSWE.Networking
|
||||
if (!hasBoundaryRates)
|
||||
{
|
||||
lastForcedCellCount = 0;
|
||||
lastForcingStatus = "disabled:no_active_source_or_sink_levels";
|
||||
lastForcingStatus = "disabled:no_active_source_or_sink_levels_legacy";
|
||||
simulator.ClearExternalDepthRateMap();
|
||||
simulator.SetExternalDepthRate(0.0f);
|
||||
return;
|
||||
@@ -687,13 +1028,13 @@ namespace FloodSWE.Networking
|
||||
{
|
||||
simulator.SetExternalDepthRate(0.0f);
|
||||
lastForcedCellCount = forcedCellCount;
|
||||
lastForcingStatus = "localized";
|
||||
lastForcingStatus = "legacy_localized";
|
||||
Debug.Log($"SweServerRuntime: applied localized boundary forcing cells={forcedCellCount}");
|
||||
return;
|
||||
}
|
||||
|
||||
lastForcedCellCount = 0;
|
||||
lastForcingStatus = "disabled:boundary_masks_zero_or_apply_failed";
|
||||
lastForcingStatus = "disabled:boundary_masks_zero_or_apply_failed_legacy";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -707,7 +1048,7 @@ namespace FloodSWE.Networking
|
||||
}
|
||||
|
||||
lastForcedCellCount = 0;
|
||||
lastForcingStatus = "disabled:masks_unavailable";
|
||||
lastForcingStatus = "disabled:masks_unavailable_legacy";
|
||||
}
|
||||
|
||||
// Strict mode: no masks means no forcing. This prevents non-physical uniform flooding.
|
||||
@@ -715,6 +1056,65 @@ namespace FloodSWE.Networking
|
||||
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)
|
||||
@@ -727,6 +1127,303 @@ namespace FloodSWE.Networking
|
||||
return x == 0 || y == 0 || x == resolution - 1 || y == resolution - 1;
|
||||
}
|
||||
|
||||
private static void CollectIdsFromRefs(SweBoundaryTileIdRef[] refs, HashSet<int> 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<int, int[]> outGroups,
|
||||
HashSet<int> 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<string>();
|
||||
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<SweBoundaryProfile>(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)
|
||||
@@ -748,7 +1445,7 @@ namespace FloodSWE.Networking
|
||||
}
|
||||
|
||||
float clamped = Mathf.Max(0.0f, defaultSourceLevel);
|
||||
sourceLevels[defaultSourceId] = clamped;
|
||||
ApplyLegacySourceLevel(defaultSourceId, clamped);
|
||||
Debug.Log($"SweServerRuntime: applied startup source id={defaultSourceId} level={clamped:0.###}");
|
||||
}
|
||||
|
||||
@@ -773,6 +1470,7 @@ namespace FloodSWE.Networking
|
||||
porosityValues = porosity,
|
||||
sourceLevels = CloneDictionary(sourceLevels),
|
||||
sinkLevels = CloneDictionary(sinkLevels),
|
||||
boundaryProfiles = CloneBoundaryProfiles(boundaryProfiles),
|
||||
};
|
||||
|
||||
checkpoints[checkpointName] = state;
|
||||
@@ -803,6 +1501,7 @@ namespace FloodSWE.Networking
|
||||
|
||||
sourceLevels.Clear();
|
||||
sinkLevels.Clear();
|
||||
boundaryProfiles.Clear();
|
||||
|
||||
if (state.sourceLevels != null)
|
||||
{
|
||||
@@ -820,7 +1519,21 @@ namespace FloodSWE.Networking
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -840,6 +1553,22 @@ namespace FloodSWE.Networking
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static SweBoundaryProfile[] CloneBoundaryProfiles(Dictionary<string, SweBoundaryProfile> source)
|
||||
{
|
||||
if (source == null || source.Count == 0)
|
||||
{
|
||||
return Array.Empty<SweBoundaryProfile>();
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1003,6 +1732,7 @@ namespace FloodSWE.Networking
|
||||
public float[] porosityValues;
|
||||
public Dictionary<int, float> sourceLevels;
|
||||
public Dictionary<int, float> sinkLevels;
|
||||
public SweBoundaryProfile[] boundaryProfiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user