using System; using System.Collections.Generic; using UnityEngine; namespace FloodSWE.Networking { [Serializable] 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 tileLookup; [NonSerialized] private Dictionary boundaryLookup; [NonSerialized] private Dictionary sourceLookup; [NonSerialized] private Dictionary sinkLookup; public static bool TryLoad(string json, out SweBoundaryManifest manifest) { manifest = null; if (string.IsNullOrWhiteSpace(json)) { return false; } try { manifest = JsonUtility.FromJson(json); if (manifest == null) { return false; } manifest.BuildLookup(); return true; } catch (Exception ex) { Debug.LogWarning($"SweBoundaryManifest: failed to parse manifest. {ex.Message}"); return false; } } public bool TryGetTile(string lod, int tileX, int tileY, out SweBoundaryTile tile) { tile = null; if (tileLookup == null) { BuildLookup(); } return tileLookup != null && tileLookup.TryGetValue(MakeTileKey(lod, tileX, tileY), out tile); } public bool TryGetSource(int id, out SweBoundarySource source) { source = null; if (sourceLookup == null) { BuildLookup(); } 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; if (sinkLookup == null) { BuildLookup(); } return sinkLookup != null && sinkLookup.TryGetValue(id, out sink); } public static int ParseLod(string lod) { if (string.IsNullOrWhiteSpace(lod)) { return 0; } string value = lod.Trim().ToLowerInvariant(); if (value.StartsWith("lod", StringComparison.Ordinal)) { value = value.Substring(3); } return int.TryParse(value, out int parsed) ? parsed : 0; } private void BuildLookup() { tileLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); boundaryLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); sourceLookup = new Dictionary(); sinkLookup = new Dictionary(); if (tiles == null) { tileLookup = tileLookup ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } if (tiles != null) { for (int i = 0; i < tiles.Length; i++) { SweBoundaryTile tile = tiles[i]; if (tile == null) { continue; } tileLookup[MakeTileKey(tile.lod, tile.tile_x, tile.tile_y)] = tile; } } 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++) { SweBoundarySource source = sources[i]; 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), }; } } } } if (sinks != null) { for (int i = 0; i < sinks.Length; i++) { SweBoundarySink sink = sinks[i]; 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), }; } } } } } private static string MakeTileKey(string lod, int tileX, int tileY) { 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] public sealed class SweBoundarySource { public int id; public int tile_count; public int total_pixels; public SweBoundaryParams @params; } [Serializable] public sealed class SweBoundarySink { public int id; public int tile_count; public int total_pixels; public SweBoundaryParams @params; } [Serializable] public sealed class SweBoundaryTile { 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[] boundary_sink_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) || (boundary_sink_cells != null && boundary_sink_cells.Length > 0) || (source_area_cells != null && source_area_cells.Length > 0) || (sink_cells != null && sink_cells.Length > 0); } } } [Serializable] public sealed class SweBoundaryParams { public string name; public string mode; public float trigger_level_m; public float max_outflow_m3s; public bool IsMode(string value) { return !string.IsNullOrWhiteSpace(mode) && string.Equals(mode.Trim(), value, StringComparison.OrdinalIgnoreCase); } } [Serializable] public sealed class SweBoundaryTileIdRef { 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, }; } } }