Rework SWE boundary control to ghost inflows and boundary profiles
This commit is contained in:
@@ -10,6 +10,9 @@ Texture2D<float> _WaterEast;
|
||||
Texture2D<float2> _VelEast;
|
||||
Texture2D<float> _WaterWest;
|
||||
Texture2D<float2> _VelWest;
|
||||
Texture2D<float> _GhostOverrideMask;
|
||||
Texture2D<float> _GhostOverrideWater;
|
||||
Texture2D<float2> _GhostOverrideVel;
|
||||
|
||||
RWTexture2D<float> _WaterOut;
|
||||
RWTexture2D<float2> _VelOut;
|
||||
@@ -19,6 +22,7 @@ int _HasNorth;
|
||||
int _HasSouth;
|
||||
int _HasEast;
|
||||
int _HasWest;
|
||||
int _UseGhostOverride;
|
||||
|
||||
[numthreads(8, 8, 1)]
|
||||
void GhostExchange(uint3 id : SV_DispatchThreadID)
|
||||
@@ -73,6 +77,16 @@ void GhostExchange(uint3 id : SV_DispatchThreadID)
|
||||
}
|
||||
}
|
||||
|
||||
if (_UseGhostOverride == 1 && (g.x == 0 || g.x == size - 1 || g.y == 0 || g.y == size - 1))
|
||||
{
|
||||
float mask = _GhostOverrideMask[g];
|
||||
if (mask > 0.5)
|
||||
{
|
||||
h = _GhostOverrideWater[g];
|
||||
v = _GhostOverrideVel[g];
|
||||
}
|
||||
}
|
||||
|
||||
if (h != h || h < 0.0)
|
||||
{
|
||||
h = 0.0;
|
||||
|
||||
@@ -127,14 +127,20 @@ namespace FloodSWE.IO
|
||||
{
|
||||
simulator.porosity = data.Porosity;
|
||||
}
|
||||
simulator.sourceIdMask = data.SourceIds;
|
||||
simulator.sinkIdMask = data.SinkIds;
|
||||
simulator.ApplyTileStaticData(
|
||||
data.Height,
|
||||
data.Porosity,
|
||||
data.SourceIds,
|
||||
data.SinkIds,
|
||||
data.Resolution,
|
||||
data.TileSizeM);
|
||||
|
||||
if (verboseDiagnostics)
|
||||
{
|
||||
Debug.Log(
|
||||
$"SweTileLoader: applied tile {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
|
||||
$"to simulator (height={DescribeTexture(data.Height)}, porosity={DescribeTexture(data.Porosity)}, " +
|
||||
$"to simulator (res={data.Resolution}, tileSize={data.TileSizeM:0.###}m, " +
|
||||
$"height={DescribeTexture(data.Height)}, porosity={DescribeTexture(data.Porosity)}, " +
|
||||
$"sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)}).");
|
||||
}
|
||||
|
||||
@@ -223,6 +229,8 @@ namespace FloodSWE.IO
|
||||
lod,
|
||||
tileX,
|
||||
tileY,
|
||||
ParseFloat(parts[7], 1000.0f),
|
||||
ParseInt(parts[8], 256),
|
||||
NormalizePath(parts[9]),
|
||||
NormalizePath(parts[10]),
|
||||
NormalizePath(parts[11])
|
||||
@@ -405,6 +413,8 @@ namespace FloodSWE.IO
|
||||
var sb = new StringBuilder(256);
|
||||
sb.Append("tile=").Append(requestedTile)
|
||||
.Append("; index=").Append(indexRecordCount)
|
||||
.Append("; res=").Append(record.Resolution)
|
||||
.Append("; tileSize=").Append(record.TileSizeM.ToString("0.###", CultureInfo.InvariantCulture))
|
||||
.Append("; height=").Append(DescribeTexture(height))
|
||||
.Append("; porosity=").Append(DescribeTexture(porosity))
|
||||
.Append("; buildings=").Append(DescribeTexture(buildings))
|
||||
@@ -468,6 +478,24 @@ namespace FloodSWE.IO
|
||||
return $"{texture.width}x{texture.height},readable={texture.isReadable}";
|
||||
}
|
||||
|
||||
private static float ParseFloat(string raw, float fallback)
|
||||
{
|
||||
if (float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static int ParseInt(string raw, int fallback)
|
||||
{
|
||||
if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private readonly struct TileKey : IEquatable<TileKey>
|
||||
{
|
||||
public readonly string Lod;
|
||||
@@ -509,15 +537,27 @@ namespace FloodSWE.IO
|
||||
public readonly string Lod;
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly float TileSizeM;
|
||||
public readonly int Resolution;
|
||||
public readonly string HeightPath;
|
||||
public readonly string PorosityPath;
|
||||
public readonly string BuildingPath;
|
||||
|
||||
public TileRecord(string lod, int x, int y, string heightPath, string porosityPath, string buildingPath)
|
||||
public TileRecord(
|
||||
string lod,
|
||||
int x,
|
||||
int y,
|
||||
float tileSizeM,
|
||||
int resolution,
|
||||
string heightPath,
|
||||
string porosityPath,
|
||||
string buildingPath)
|
||||
{
|
||||
Lod = lod;
|
||||
X = x;
|
||||
Y = y;
|
||||
TileSizeM = tileSizeM;
|
||||
Resolution = resolution;
|
||||
HeightPath = heightPath;
|
||||
PorosityPath = porosityPath;
|
||||
BuildingPath = buildingPath;
|
||||
@@ -530,6 +570,8 @@ namespace FloodSWE.IO
|
||||
public readonly string Lod;
|
||||
public readonly int TileX;
|
||||
public readonly int TileY;
|
||||
public readonly float TileSizeM;
|
||||
public readonly int Resolution;
|
||||
public readonly Texture2D Height;
|
||||
public readonly Texture2D Porosity;
|
||||
public readonly Texture2D Buildings;
|
||||
@@ -547,6 +589,8 @@ namespace FloodSWE.IO
|
||||
Lod = record.Lod;
|
||||
TileX = record.X;
|
||||
TileY = record.Y;
|
||||
TileSizeM = record.TileSizeM;
|
||||
Resolution = record.Resolution;
|
||||
Height = height;
|
||||
Porosity = porosity;
|
||||
Buildings = buildings;
|
||||
|
||||
@@ -79,6 +79,13 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
private Texture2D externalDepthRateMap;
|
||||
private float[] externalDepthRateMapData;
|
||||
private bool useExternalDepthRateMap;
|
||||
private Texture2D ghostOverrideMask;
|
||||
private Texture2D ghostOverrideWater;
|
||||
private Texture2D ghostOverrideVelocity;
|
||||
private float[] ghostOverrideMaskData;
|
||||
private float[] ghostOverrideWaterData;
|
||||
private Vector2[] ghostOverrideVelocityData;
|
||||
private bool useGhostBoundaryOverrides;
|
||||
private RenderTexture clampMask;
|
||||
private RenderTexture dummyRw;
|
||||
|
||||
@@ -163,6 +170,47 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
externalDepthRateMps = metersPerSecond;
|
||||
}
|
||||
|
||||
public void ApplyTileStaticData(
|
||||
Texture2D terrain,
|
||||
Texture2D porosityTexture,
|
||||
Texture2D sourceMask,
|
||||
Texture2D sinkMaskTexture,
|
||||
int resolution,
|
||||
float tileSizeM)
|
||||
{
|
||||
if (terrain != null)
|
||||
{
|
||||
terrainHeight = terrain;
|
||||
}
|
||||
if (porosityTexture != null)
|
||||
{
|
||||
porosity = porosityTexture;
|
||||
}
|
||||
sourceIdMask = sourceMask;
|
||||
sinkIdMask = sinkMaskTexture;
|
||||
|
||||
int targetResolution = Mathf.Max(4, resolution);
|
||||
float targetTileSize = Mathf.Max(1.0f, tileSizeM);
|
||||
bool requiresReinit = targetResolution != gridRes || Mathf.Abs(targetTileSize - tileSizeMeters) > 0.001f;
|
||||
gridRes = targetResolution;
|
||||
tileSizeMeters = targetTileSize;
|
||||
|
||||
if (!isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresReinit)
|
||||
{
|
||||
Release();
|
||||
Initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveStaticTextures();
|
||||
ResetSimulationState();
|
||||
}
|
||||
|
||||
public bool SetExternalDepthRateMap(float[] metersPerSecondPerCell)
|
||||
{
|
||||
if (!isInitialized || metersPerSecondPerCell == null || metersPerSecondPerCell.Length != gridRes * gridRes)
|
||||
@@ -192,6 +240,72 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
useExternalDepthRateMap = false;
|
||||
}
|
||||
|
||||
public bool SetGhostBoundaryOverrides(int[] ghostIndices, float[] waterLevels, Vector2[] velocities)
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ghostIndices == null || waterLevels == null || velocities == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ghostIndices.Length == 0 || ghostIndices.Length != waterLevels.Length || ghostIndices.Length != velocities.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureGhostOverrideTextures();
|
||||
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
|
||||
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
|
||||
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
|
||||
|
||||
bool any = false;
|
||||
int maxIndex = ghostOverrideMaskData.Length - 1;
|
||||
for (int i = 0; i < ghostIndices.Length; i++)
|
||||
{
|
||||
int idx = ghostIndices[i];
|
||||
if (idx < 0 || idx > maxIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
ghostOverrideMaskData[idx] = 1.0f;
|
||||
ghostOverrideWaterData[idx] = Mathf.Max(0.0f, waterLevels[i]);
|
||||
ghostOverrideVelocityData[idx] = velocities[i];
|
||||
}
|
||||
|
||||
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
|
||||
ghostOverrideMask.Apply(false, false);
|
||||
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
|
||||
ghostOverrideWater.Apply(false, false);
|
||||
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
|
||||
ghostOverrideVelocity.Apply(false, false);
|
||||
useGhostBoundaryOverrides = any;
|
||||
return any;
|
||||
}
|
||||
|
||||
public void ClearGhostBoundaryOverrides()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureGhostOverrideTextures();
|
||||
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
|
||||
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
|
||||
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
|
||||
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
|
||||
ghostOverrideMask.Apply(false, false);
|
||||
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
|
||||
ghostOverrideWater.Apply(false, false);
|
||||
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
|
||||
ghostOverrideVelocity.Apply(false, false);
|
||||
useGhostBoundaryOverrides = false;
|
||||
}
|
||||
|
||||
public bool TryGetBoundaryIdMasks(out int[] sourceIds, out int[] sinkIds)
|
||||
{
|
||||
sourceIds = null;
|
||||
@@ -446,28 +560,8 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
}
|
||||
|
||||
ResolveStaticTextures();
|
||||
|
||||
ClearRenderTexture(waterB);
|
||||
ClearRenderTexture(velB);
|
||||
ClearRenderTexture(waterGhost);
|
||||
ClearRenderTexture(velGhost);
|
||||
|
||||
fluxShader.SetInt("_GridRes", gridRes);
|
||||
fluxShader.SetFloat("_InitDepthLeft", initialDepthLeft);
|
||||
fluxShader.SetFloat("_InitDepthRight", initialDepthRight);
|
||||
fluxShader.SetVector("_InitVelocity", initialVelocity);
|
||||
fluxShader.SetInt("_InitDamBreak", damBreak ? 1 : 0);
|
||||
fluxShader.SetTexture(initKernel, "_WaterOut", waterA);
|
||||
fluxShader.SetTexture(initKernel, "_VelOut", velA);
|
||||
DispatchKernel(fluxShader, initKernel, gridRes, gridRes);
|
||||
|
||||
useA = true;
|
||||
debugWater = CurrentWater;
|
||||
debugVelocity = CurrentVelocity;
|
||||
|
||||
lastMaxDepth = Mathf.Max(initialDepthLeft, initialDepthRight);
|
||||
lastMaxSpeed = Mathf.Sqrt(Gravity * Mathf.Max(lastMaxDepth, 0.0f));
|
||||
isInitialized = true;
|
||||
ResetSimulationState();
|
||||
}
|
||||
|
||||
private void Release()
|
||||
@@ -490,6 +584,68 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
clampMask = null;
|
||||
dummyRw = null;
|
||||
|
||||
ReleaseResolvedStaticTextures();
|
||||
|
||||
if (externalDepthRateMap != null)
|
||||
{
|
||||
Destroy(externalDepthRateMap);
|
||||
}
|
||||
if (ghostOverrideMask != null)
|
||||
{
|
||||
Destroy(ghostOverrideMask);
|
||||
}
|
||||
if (ghostOverrideWater != null)
|
||||
{
|
||||
Destroy(ghostOverrideWater);
|
||||
}
|
||||
if (ghostOverrideVelocity != null)
|
||||
{
|
||||
Destroy(ghostOverrideVelocity);
|
||||
}
|
||||
|
||||
resolvedTerrain = null;
|
||||
resolvedPorosity = null;
|
||||
resolvedSourceIdMask = null;
|
||||
resolvedSinkIdMask = null;
|
||||
externalDepthRateMap = null;
|
||||
externalDepthRateMapData = null;
|
||||
useExternalDepthRateMap = false;
|
||||
ghostOverrideMask = null;
|
||||
ghostOverrideWater = null;
|
||||
ghostOverrideVelocity = null;
|
||||
ghostOverrideMaskData = null;
|
||||
ghostOverrideWaterData = null;
|
||||
ghostOverrideVelocityData = null;
|
||||
useGhostBoundaryOverrides = false;
|
||||
}
|
||||
|
||||
private void ResolveStaticTextures()
|
||||
{
|
||||
ReleaseResolvedStaticTextures();
|
||||
resolvedTerrain = ResolveTerrainTexture();
|
||||
resolvedPorosity = ResolvePorosityTexture();
|
||||
resolvedSourceIdMask = ResolveBoundaryMaskTexture(sourceIdMask, "SWE_SourceIds");
|
||||
resolvedSinkIdMask = ResolveBoundaryMaskTexture(sinkIdMask, "SWE_SinkIds");
|
||||
useExternalDepthRateMap = false;
|
||||
EnsureExternalDepthRateMap();
|
||||
Array.Clear(externalDepthRateMapData, 0, externalDepthRateMapData.Length);
|
||||
externalDepthRateMap.SetPixelData(externalDepthRateMapData, 0);
|
||||
externalDepthRateMap.Apply(false, false);
|
||||
EnsureGhostOverrideTextures();
|
||||
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
|
||||
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
|
||||
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
|
||||
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
|
||||
ghostOverrideMask.Apply(false, false);
|
||||
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
|
||||
ghostOverrideWater.Apply(false, false);
|
||||
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
|
||||
ghostOverrideVelocity.Apply(false, false);
|
||||
useGhostBoundaryOverrides = false;
|
||||
}
|
||||
|
||||
private void ReleaseResolvedStaticTextures()
|
||||
{
|
||||
if (resolvedTerrain != null && resolvedTerrain != terrainHeight)
|
||||
{
|
||||
Destroy(resolvedTerrain);
|
||||
@@ -509,29 +665,38 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
{
|
||||
Destroy(resolvedSinkIdMask);
|
||||
}
|
||||
|
||||
if (externalDepthRateMap != null)
|
||||
{
|
||||
Destroy(externalDepthRateMap);
|
||||
}
|
||||
|
||||
resolvedTerrain = null;
|
||||
resolvedPorosity = null;
|
||||
resolvedSourceIdMask = null;
|
||||
resolvedSinkIdMask = null;
|
||||
externalDepthRateMap = null;
|
||||
externalDepthRateMapData = null;
|
||||
useExternalDepthRateMap = false;
|
||||
}
|
||||
|
||||
private void ResolveStaticTextures()
|
||||
private void ResetSimulationState()
|
||||
{
|
||||
resolvedTerrain = ResolveTerrainTexture();
|
||||
resolvedPorosity = ResolvePorosityTexture();
|
||||
resolvedSourceIdMask = ResolveBoundaryMaskTexture(sourceIdMask, "SWE_SourceIds");
|
||||
resolvedSinkIdMask = ResolveBoundaryMaskTexture(sinkIdMask, "SWE_SinkIds");
|
||||
useExternalDepthRateMap = false;
|
||||
EnsureExternalDepthRateMap();
|
||||
if (waterA == null || velA == null || waterB == null || velB == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearRenderTexture(waterA);
|
||||
ClearRenderTexture(velA);
|
||||
ClearRenderTexture(waterB);
|
||||
ClearRenderTexture(velB);
|
||||
ClearRenderTexture(waterGhost);
|
||||
ClearRenderTexture(velGhost);
|
||||
|
||||
fluxShader.SetInt("_GridRes", gridRes);
|
||||
fluxShader.SetFloat("_InitDepthLeft", initialDepthLeft);
|
||||
fluxShader.SetFloat("_InitDepthRight", initialDepthRight);
|
||||
fluxShader.SetVector("_InitVelocity", initialVelocity);
|
||||
fluxShader.SetInt("_InitDamBreak", damBreak ? 1 : 0);
|
||||
fluxShader.SetTexture(initKernel, "_WaterOut", waterA);
|
||||
fluxShader.SetTexture(initKernel, "_VelOut", velA);
|
||||
DispatchKernel(fluxShader, initKernel, gridRes, gridRes);
|
||||
|
||||
useA = true;
|
||||
tickIndex = 0;
|
||||
pendingMassScale = 1.0f;
|
||||
lastMaxDepth = Mathf.Max(initialDepthLeft, initialDepthRight);
|
||||
lastMaxSpeed = Mathf.Sqrt(Gravity * Mathf.Max(lastMaxDepth, 0.0f));
|
||||
debugWater = CurrentWater;
|
||||
debugVelocity = CurrentVelocity;
|
||||
}
|
||||
|
||||
private Texture2D ResolveTerrainTexture()
|
||||
@@ -698,11 +863,17 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
|
||||
private void DispatchGhost()
|
||||
{
|
||||
if (ghostOverrideMask == null || ghostOverrideWater == null || ghostOverrideVelocity == null)
|
||||
{
|
||||
EnsureGhostOverrideTextures();
|
||||
}
|
||||
|
||||
ghostExchangeShader.SetInt("_GridRes", gridRes);
|
||||
ghostExchangeShader.SetInt("_HasNorth", 0);
|
||||
ghostExchangeShader.SetInt("_HasSouth", 0);
|
||||
ghostExchangeShader.SetInt("_HasEast", 0);
|
||||
ghostExchangeShader.SetInt("_HasWest", 0);
|
||||
ghostExchangeShader.SetInt("_UseGhostOverride", useGhostBoundaryOverrides ? 1 : 0);
|
||||
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_WaterIn", CurrentWater);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_VelIn", CurrentVelocity);
|
||||
@@ -716,6 +887,9 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_VelWest", CurrentVelocity);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_WaterOut", waterGhost);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_VelOut", velGhost);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideMask", ghostOverrideMask);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideWater", ghostOverrideWater);
|
||||
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideVel", ghostOverrideVelocity);
|
||||
|
||||
DispatchKernel(ghostExchangeShader, ghostKernel, gridRes + 2, gridRes + 2);
|
||||
}
|
||||
@@ -1112,6 +1286,64 @@ public sealed class SweTileSimulator : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureGhostOverrideTextures()
|
||||
{
|
||||
int ghostRes = gridRes + 2;
|
||||
int length = ghostRes * ghostRes;
|
||||
|
||||
if (ghostOverrideMask == null || ghostOverrideMask.width != ghostRes || ghostOverrideMask.height != ghostRes)
|
||||
{
|
||||
if (ghostOverrideMask != null)
|
||||
{
|
||||
Destroy(ghostOverrideMask);
|
||||
}
|
||||
|
||||
ghostOverrideMask = CreateTextureFromFloats(new float[length], ghostRes, ghostRes, "SWE_GhostOverrideMask");
|
||||
ghostOverrideMaskData = new float[length];
|
||||
}
|
||||
else if (ghostOverrideMaskData == null || ghostOverrideMaskData.Length != length)
|
||||
{
|
||||
ghostOverrideMaskData = new float[length];
|
||||
}
|
||||
|
||||
if (ghostOverrideWater == null || ghostOverrideWater.width != ghostRes || ghostOverrideWater.height != ghostRes)
|
||||
{
|
||||
if (ghostOverrideWater != null)
|
||||
{
|
||||
Destroy(ghostOverrideWater);
|
||||
}
|
||||
|
||||
ghostOverrideWater = CreateTextureFromFloats(new float[length], ghostRes, ghostRes, "SWE_GhostOverrideWater");
|
||||
ghostOverrideWaterData = new float[length];
|
||||
}
|
||||
else if (ghostOverrideWaterData == null || ghostOverrideWaterData.Length != length)
|
||||
{
|
||||
ghostOverrideWaterData = new float[length];
|
||||
}
|
||||
|
||||
if (ghostOverrideVelocity == null || ghostOverrideVelocity.width != ghostRes || ghostOverrideVelocity.height != ghostRes)
|
||||
{
|
||||
if (ghostOverrideVelocity != null)
|
||||
{
|
||||
Destroy(ghostOverrideVelocity);
|
||||
}
|
||||
|
||||
ghostOverrideVelocity = new Texture2D(ghostRes, ghostRes, TextureFormat.RGFloat, false, true)
|
||||
{
|
||||
name = "SWE_GhostOverrideVelocity",
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Point
|
||||
};
|
||||
ghostOverrideVelocityData = new Vector2[length];
|
||||
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
|
||||
ghostOverrideVelocity.Apply(false, false);
|
||||
}
|
||||
else if (ghostOverrideVelocityData == null || ghostOverrideVelocityData.Length != length)
|
||||
{
|
||||
ghostOverrideVelocityData = new Vector2[length];
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] ReadIdTexture(Texture2D texture)
|
||||
{
|
||||
if (texture == null || !texture.isReadable)
|
||||
|
||||
@@ -22,12 +22,20 @@ namespace FloodSWE.Networking
|
||||
private IPEndPoint serverEndpoint;
|
||||
private float lastAckTimeUnscaled = -1.0f;
|
||||
private float lastHelloSentTimeUnscaled = -1.0f;
|
||||
private float lastStateTimeUnscaled = -1.0f;
|
||||
private string lastAckMessage = "";
|
||||
private SweBoundaryStateMessage lastBoundaryState;
|
||||
private bool connectRequested;
|
||||
|
||||
public event Action<SweBoundaryStateMessage> BoundaryStateReceived;
|
||||
|
||||
public bool HasReceivedAck => lastAckTimeUnscaled >= 0.0f;
|
||||
public float LastAckAgeSeconds => HasReceivedAck ? Time.unscaledTime - lastAckTimeUnscaled : float.PositiveInfinity;
|
||||
public string LastAckMessage => lastAckMessage;
|
||||
public bool HasBoundaryState => lastStateTimeUnscaled >= 0.0f;
|
||||
public float LastBoundaryStateAgeSeconds =>
|
||||
HasBoundaryState ? Time.unscaledTime - lastStateTimeUnscaled : float.PositiveInfinity;
|
||||
public SweBoundaryStateMessage LastBoundaryState => lastBoundaryState;
|
||||
public bool IsConnectionAlive => HasReceivedAck && LastAckAgeSeconds <= Mathf.Max(0.1f, ackTimeoutSeconds);
|
||||
public bool IsWaitingForAck => connectRequested && !IsConnectionAlive && !HasReceivedAck;
|
||||
|
||||
@@ -46,7 +54,7 @@ namespace FloodSWE.Networking
|
||||
|
||||
private void Update()
|
||||
{
|
||||
PollAcks();
|
||||
PollMessages();
|
||||
TickKeepAlive();
|
||||
}
|
||||
|
||||
@@ -89,7 +97,9 @@ namespace FloodSWE.Networking
|
||||
|
||||
serverEndpoint = null;
|
||||
lastAckTimeUnscaled = -1.0f;
|
||||
lastStateTimeUnscaled = -1.0f;
|
||||
lastAckMessage = "";
|
||||
lastBoundaryState = null;
|
||||
connectRequested = false;
|
||||
}
|
||||
|
||||
@@ -104,21 +114,79 @@ namespace FloodSWE.Networking
|
||||
|
||||
public void SetSourceLevel(int sourceId, float level)
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "set_source_level",
|
||||
sourceId = sourceId,
|
||||
sourceLevel = level,
|
||||
});
|
||||
SetBoundaryProfile(
|
||||
"source_area",
|
||||
sourceId,
|
||||
true,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
Mathf.Max(0.0f, level));
|
||||
}
|
||||
|
||||
public void SetSinkLevel(int sinkId, float level)
|
||||
{
|
||||
SetBoundaryProfile(
|
||||
"sink",
|
||||
sinkId,
|
||||
true,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
-Mathf.Max(0.0f, level));
|
||||
}
|
||||
|
||||
public void SetBoundaryProfile(
|
||||
string boundaryKind,
|
||||
int boundaryId,
|
||||
bool enabled,
|
||||
float waterLevelM,
|
||||
float velocityUMps,
|
||||
float velocityVMps,
|
||||
float depthRateMps)
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "set_sink_level",
|
||||
sinkId = sinkId,
|
||||
sinkLevel = level,
|
||||
command = "set_boundary_profile",
|
||||
boundaryKind = boundaryKind,
|
||||
boundaryId = boundaryId,
|
||||
enabled = enabled ? 1 : 0,
|
||||
waterLevelM = waterLevelM,
|
||||
velocityUMps = velocityUMps,
|
||||
velocityVMps = velocityVMps,
|
||||
depthRateMps = depthRateMps,
|
||||
});
|
||||
}
|
||||
|
||||
public void SetBoundariesBulk(SweBoundaryProfile[] profiles, bool replaceAll = false)
|
||||
{
|
||||
if (profiles == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "set_boundaries_bulk",
|
||||
replaceAll = replaceAll,
|
||||
boundaries = profiles,
|
||||
});
|
||||
}
|
||||
|
||||
public void RequestBoundaryConfig()
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "get_boundary_config",
|
||||
});
|
||||
}
|
||||
|
||||
public void SubscribeBoundaryUpdates(bool subscribe = true)
|
||||
{
|
||||
Send(new SweControlCommand
|
||||
{
|
||||
command = "subscribe_boundary_updates",
|
||||
subscribe = subscribe,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,7 +282,7 @@ namespace FloodSWE.Networking
|
||||
socket.Send(payload, payload.Length, serverEndpoint);
|
||||
}
|
||||
|
||||
private void PollAcks()
|
||||
private void PollMessages()
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
@@ -234,17 +302,33 @@ namespace FloodSWE.Networking
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SweUdpProtocol.TryDecodeAck(payload, out string message))
|
||||
if (SweUdpProtocol.TryDecodeAck(payload, out string message))
|
||||
{
|
||||
lastAckTimeUnscaled = Time.unscaledTime;
|
||||
lastAckMessage = message ?? "";
|
||||
|
||||
if (verboseLogging)
|
||||
{
|
||||
Debug.Log($"SweQuestControlClient: ack from {sender}: {lastAckMessage}");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
lastAckTimeUnscaled = Time.unscaledTime;
|
||||
lastAckMessage = message ?? "";
|
||||
|
||||
if (verboseLogging)
|
||||
if (SweUdpProtocol.TryDecodeState(payload, out SweBoundaryStateMessage state))
|
||||
{
|
||||
Debug.Log($"SweQuestControlClient: ack from {sender}: {lastAckMessage}");
|
||||
lastStateTimeUnscaled = Time.unscaledTime;
|
||||
lastBoundaryState = state;
|
||||
BoundaryStateReceived?.Invoke(state);
|
||||
|
||||
if (verboseLogging)
|
||||
{
|
||||
int count = state != null && state.boundaries != null ? state.boundaries.Length : 0;
|
||||
Debug.Log(
|
||||
$"SweQuestControlClient: boundary state from {sender}: type={state?.messageType} entries={count}");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,19 @@ namespace FloodSWE.Networking
|
||||
public sealed class SweBoundaryManifest
|
||||
{
|
||||
public int schema_version;
|
||||
public string boundary_inflow_mask_dir;
|
||||
public string source_area_mask_dir;
|
||||
public string sink_mask_dir;
|
||||
public string boundary_inflow_params_toml;
|
||||
public string source_area_params_toml;
|
||||
public string sink_params_toml;
|
||||
public SweBoundarySource[] sources;
|
||||
public SweBoundarySink[] sinks;
|
||||
public SweBoundaryDefinition[] boundaries;
|
||||
public SweBoundaryTile[] tiles;
|
||||
|
||||
[NonSerialized] private Dictionary<string, SweBoundaryTile> tileLookup;
|
||||
[NonSerialized] private Dictionary<string, SweBoundaryDefinition> boundaryLookup;
|
||||
[NonSerialized] private Dictionary<int, SweBoundarySource> sourceLookup;
|
||||
[NonSerialized] private Dictionary<int, SweBoundarySink> sinkLookup;
|
||||
|
||||
@@ -64,6 +72,17 @@ namespace FloodSWE.Networking
|
||||
return sourceLookup != null && sourceLookup.TryGetValue(id, out source);
|
||||
}
|
||||
|
||||
public bool TryGetBoundary(string kind, int id, out SweBoundaryDefinition boundary)
|
||||
{
|
||||
boundary = null;
|
||||
if (boundaryLookup == null)
|
||||
{
|
||||
BuildLookup();
|
||||
}
|
||||
|
||||
return boundaryLookup != null && boundaryLookup.TryGetValue(MakeBoundaryKey(kind, id), out boundary);
|
||||
}
|
||||
|
||||
public bool TryGetSink(int id, out SweBoundarySink sink)
|
||||
{
|
||||
sink = null;
|
||||
@@ -94,6 +113,7 @@ namespace FloodSWE.Networking
|
||||
private void BuildLookup()
|
||||
{
|
||||
tileLookup = new Dictionary<string, SweBoundaryTile>(StringComparer.OrdinalIgnoreCase);
|
||||
boundaryLookup = new Dictionary<string, SweBoundaryDefinition>(StringComparer.OrdinalIgnoreCase);
|
||||
sourceLookup = new Dictionary<int, SweBoundarySource>();
|
||||
sinkLookup = new Dictionary<int, SweBoundarySink>();
|
||||
if (tiles == null)
|
||||
@@ -115,6 +135,25 @@ namespace FloodSWE.Networking
|
||||
}
|
||||
}
|
||||
|
||||
if (boundaries != null && boundaries.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < boundaries.Length; i++)
|
||||
{
|
||||
SweBoundaryDefinition boundary = boundaries[i];
|
||||
if (boundary == null || boundary.id <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (boundary.default_state == null)
|
||||
{
|
||||
boundary.default_state = SweBoundaryDefaultState.DefaultFor(boundary.kind, boundary.@params);
|
||||
}
|
||||
|
||||
boundaryLookup[MakeBoundaryKey(boundary.kind, boundary.id)] = boundary;
|
||||
}
|
||||
}
|
||||
|
||||
if (sources != null)
|
||||
{
|
||||
for (int i = 0; i < sources.Length; i++)
|
||||
@@ -123,6 +162,19 @@ namespace FloodSWE.Networking
|
||||
if (source != null)
|
||||
{
|
||||
sourceLookup[source.id] = source;
|
||||
string key = MakeBoundaryKey("boundary_inflow", source.id);
|
||||
if (!boundaryLookup.ContainsKey(key))
|
||||
{
|
||||
boundaryLookup[key] = new SweBoundaryDefinition
|
||||
{
|
||||
kind = "boundary_inflow",
|
||||
id = source.id,
|
||||
tile_count = source.tile_count,
|
||||
total_pixels = source.total_pixels,
|
||||
@params = source.@params,
|
||||
default_state = SweBoundaryDefaultState.DefaultFor("boundary_inflow", source.@params),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +187,19 @@ namespace FloodSWE.Networking
|
||||
if (sink != null)
|
||||
{
|
||||
sinkLookup[sink.id] = sink;
|
||||
string key = MakeBoundaryKey("sink", sink.id);
|
||||
if (!boundaryLookup.ContainsKey(key))
|
||||
{
|
||||
boundaryLookup[key] = new SweBoundaryDefinition
|
||||
{
|
||||
kind = "sink",
|
||||
id = sink.id,
|
||||
tile_count = sink.tile_count,
|
||||
total_pixels = sink.total_pixels,
|
||||
@params = sink.@params,
|
||||
default_state = SweBoundaryDefaultState.DefaultFor("sink", sink.@params),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +209,16 @@ namespace FloodSWE.Networking
|
||||
{
|
||||
return $"{lod}|{tileX}|{tileY}";
|
||||
}
|
||||
|
||||
private static string MakeBoundaryKey(string kind, int id)
|
||||
{
|
||||
string normalized = string.IsNullOrWhiteSpace(kind) ? "boundary_inflow" : kind.Trim().ToLowerInvariant();
|
||||
if (normalized == "source")
|
||||
{
|
||||
normalized = "boundary_inflow";
|
||||
}
|
||||
return $"{normalized}:{id}";
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -170,8 +245,30 @@ namespace FloodSWE.Networking
|
||||
public string lod;
|
||||
public int tile_x;
|
||||
public int tile_y;
|
||||
public float tile_size_m;
|
||||
public int resolution;
|
||||
public float[] bounds;
|
||||
public SweBoundaryTileIdRef[] source_ids;
|
||||
public SweBoundaryTileIdRef[] sink_ids;
|
||||
public SweBoundaryTileIdRef[] boundary_inflow_ids;
|
||||
public SweBoundaryTileIdRef[] source_area_ids;
|
||||
public SweBoundaryTileCellGroup[] boundary_cells;
|
||||
public SweBoundaryTileCellGroup[] source_area_cells;
|
||||
public SweBoundaryTileCellGroup[] sink_cells;
|
||||
public string source_id_path;
|
||||
public string sink_id_path;
|
||||
public string boundary_inflow_id_path;
|
||||
public string source_area_id_path;
|
||||
|
||||
public bool HasCellGroups
|
||||
{
|
||||
get
|
||||
{
|
||||
return (boundary_cells != null && boundary_cells.Length > 0) ||
|
||||
(source_area_cells != null && source_area_cells.Length > 0) ||
|
||||
(sink_cells != null && sink_cells.Length > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -195,4 +292,62 @@ namespace FloodSWE.Networking
|
||||
public int id;
|
||||
public int pixels;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SweBoundaryTileCellGroup
|
||||
{
|
||||
public int id;
|
||||
public int count;
|
||||
public int[] cells;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SweBoundaryDefinition
|
||||
{
|
||||
public string kind;
|
||||
public int id;
|
||||
public int tile_count;
|
||||
public int total_pixels;
|
||||
public SweBoundaryParams @params;
|
||||
public SweBoundaryDefaultState default_state;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SweBoundaryDefaultState
|
||||
{
|
||||
public bool enabled;
|
||||
public float water_level_m;
|
||||
public float velocity_u_mps;
|
||||
public float velocity_v_mps;
|
||||
public float depth_rate_mps;
|
||||
|
||||
public static SweBoundaryDefaultState DefaultFor(string kind, SweBoundaryParams parameters)
|
||||
{
|
||||
bool sinkFreeOutflow = string.Equals(kind, "sink", StringComparison.OrdinalIgnoreCase) &&
|
||||
parameters != null &&
|
||||
parameters.IsMode("free_outflow");
|
||||
return new SweBoundaryDefaultState
|
||||
{
|
||||
enabled = sinkFreeOutflow,
|
||||
water_level_m = 0.0f,
|
||||
velocity_u_mps = 0.0f,
|
||||
velocity_v_mps = 0.0f,
|
||||
depth_rate_mps = 0.0f,
|
||||
};
|
||||
}
|
||||
|
||||
public SweBoundaryProfile ToProfile(string kind, int id)
|
||||
{
|
||||
return new SweBoundaryProfile
|
||||
{
|
||||
boundaryKind = kind,
|
||||
boundaryId = id,
|
||||
enabled = enabled,
|
||||
waterLevelM = water_level_m,
|
||||
velocityUMps = velocity_u_mps,
|
||||
velocityVMps = velocity_v_mps,
|
||||
depthRateMps = depth_rate_mps,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace FloodSWE.Networking
|
||||
public const byte FrameType = 1;
|
||||
public const byte ControlType = 2;
|
||||
public const byte AckType = 3;
|
||||
public const byte StateType = 4;
|
||||
|
||||
public static byte[] EncodeFrame(HeightmapPacket packet)
|
||||
{
|
||||
@@ -156,6 +157,36 @@ namespace FloodSWE.Networking
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] EncodeState(SweBoundaryStateMessage message)
|
||||
{
|
||||
string json = JsonUtility.ToJson(message);
|
||||
byte[] utf8 = Encoding.UTF8.GetBytes(json);
|
||||
byte[] payload = new byte[utf8.Length + 1];
|
||||
payload[0] = StateType;
|
||||
Buffer.BlockCopy(utf8, 0, payload, 1, utf8.Length);
|
||||
return payload;
|
||||
}
|
||||
|
||||
public static bool TryDecodeState(byte[] data, out SweBoundaryStateMessage message)
|
||||
{
|
||||
message = null;
|
||||
if (data == null || data.Length < 2 || data[0] != StateType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = Encoding.UTF8.GetString(data, 1, data.Length - 1);
|
||||
message = JsonUtility.FromJson<SweBoundaryStateMessage>(json);
|
||||
return message != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -174,5 +205,53 @@ namespace FloodSWE.Networking
|
||||
public float v;
|
||||
public float radius;
|
||||
public float porosity;
|
||||
|
||||
public string boundaryKind;
|
||||
public int boundaryId;
|
||||
public int enabled;
|
||||
public float waterLevelM;
|
||||
public float velocityUMps;
|
||||
public float velocityVMps;
|
||||
public float depthRateMps;
|
||||
public bool replaceAll;
|
||||
public bool subscribe;
|
||||
public SweBoundaryProfile[] boundaries;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SweBoundaryProfile
|
||||
{
|
||||
public string boundaryKind;
|
||||
public int boundaryId;
|
||||
public bool enabled;
|
||||
public float waterLevelM;
|
||||
public float velocityUMps;
|
||||
public float velocityVMps;
|
||||
public float depthRateMps;
|
||||
|
||||
public SweBoundaryProfile Clone()
|
||||
{
|
||||
return new SweBoundaryProfile
|
||||
{
|
||||
boundaryKind = boundaryKind,
|
||||
boundaryId = boundaryId,
|
||||
enabled = enabled,
|
||||
waterLevelM = waterLevelM,
|
||||
velocityUMps = velocityUMps,
|
||||
velocityVMps = velocityVMps,
|
||||
depthRateMps = depthRateMps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class SweBoundaryStateMessage
|
||||
{
|
||||
public string messageType;
|
||||
public int schemaVersion;
|
||||
public string lod;
|
||||
public int tileX;
|
||||
public int tileY;
|
||||
public SweBoundaryProfile[] boundaries;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user