Rework SWE boundary control to ghost inflows and boundary profiles

This commit is contained in:
2026-02-10 22:02:20 +01:00
parent 662278858b
commit 9aa9daee79
7 changed files with 1434 additions and 96 deletions

View File

@@ -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;
}
}
}