using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEngine; namespace FloodSWE.IO { public sealed class SweTileLoader : MonoBehaviour { public enum SourceMode { Resources = 0, StreamingAssets = 1, AbsolutePath = 2, } [Header("Tile Index")] public TextAsset tileIndexCsv; public SourceMode sourceMode = SourceMode.Resources; public string tileIndexPath = "export_swe/swe_tile_index.csv"; public string fileRoot = ""; public string resourcesRoot = "export_swe"; [Header("Runtime")] public bool cacheTextures = true; public bool verboseDiagnostics = true; public bool logMissingTextures = true; private readonly Dictionary records = new Dictionary(); private readonly Dictionary textureCache = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly HashSet missingTextureWarnings = new HashSet(StringComparer.OrdinalIgnoreCase); private bool indexLoaded; private int loadAttempts; private int loadSuccesses; private string lastRequestedTileKey = "n/a"; private string lastLoadedTileKey = "n/a"; private string lastLoadSummary = "No tile load attempted yet."; private bool lastLoadSucceeded; private int indexRecordCount; private int indexSkippedRows; public string LastRequestedTileKey => lastRequestedTileKey; public string LastLoadedTileKey => lastLoadedTileKey; public string LastLoadSummary => lastLoadSummary; public bool LastLoadSucceeded => lastLoadSucceeded; public int IndexedTileCount => indexRecordCount; public int IndexSkippedRows => indexSkippedRows; public int LoadAttempts => loadAttempts; public int LoadSuccesses => loadSuccesses; public bool TryLoadTile(string lod, int tileX, int tileY, out SweTileData data) { data = default; loadAttempts++; lastRequestedTileKey = MakeTileKeyLabel(lod, tileX, tileY); EnsureIndexLoaded(); TileKey key = new TileKey(lod, tileX, tileY); if (!records.TryGetValue(key, out TileRecord record)) { lastLoadSucceeded = false; lastLoadSummary = $"tile={lastRequestedTileKey}; status=missing_in_index; indexed={records.Count}"; Debug.LogWarning($"SweTileLoader: {lastLoadSummary}"); return false; } Texture2D height = LoadTexture(record.HeightPath); Texture2D porosity = LoadTexture(record.PorosityPath); Texture2D buildings = LoadTexture(record.BuildingPath); string sourcePath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "source_ids"); string sinkPath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "sink_ids"); Texture2D sourceIds = LoadTexture(sourcePath); Texture2D sinkIds = LoadTexture(sinkPath); data = new SweTileData(record, height, porosity, buildings, sourceIds, sinkIds); bool loadedAny = height != null || porosity != null || buildings != null || sourceIds != null || sinkIds != null; lastLoadSucceeded = loadedAny; if (loadedAny) { loadSuccesses++; lastLoadedTileKey = lastRequestedTileKey; } lastLoadSummary = BuildLoadSummary( lastRequestedTileKey, record, height, porosity, buildings, sourceIds, sinkIds, sourcePath, sinkPath); if (!loadedAny) { Debug.LogWarning($"SweTileLoader: {lastLoadSummary}"); } else if (verboseDiagnostics) { Debug.Log($"SweTileLoader: {lastLoadSummary}"); } return loadedAny; } public bool ApplyToSimulator(string lod, int tileX, int tileY, SweTileSimulator simulator) { if (simulator == null) { return false; } if (!TryLoadTile(lod, tileX, tileY, out SweTileData data)) { Debug.LogWarning($"SweTileLoader: failed to load tile {lod} ({tileX},{tileY})."); return false; } if (data.Height != null) { simulator.terrainHeight = data.Height; } if (data.Porosity != null) { simulator.porosity = data.Porosity; } 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 (res={data.Resolution}, tileSize={data.TileSizeM:0.###}m, " + $"height={DescribeTexture(data.Height)}, porosity={DescribeTexture(data.Porosity)}, " + $"sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)})."); } if (data.SourceIds == null || data.SinkIds == null) { Debug.LogWarning( $"SweTileLoader: boundary mask coverage is incomplete for {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " + $"(sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)})."); } return true; } private static string BuildBoundaryMaskPath(string lod, int tileX, int tileY, string kind) { return $"export_swe/{lod}/{kind}/{kind}_{tileX}_{tileY}.exr"; } private void EnsureIndexLoaded() { if (indexLoaded) { return; } records.Clear(); indexRecordCount = 0; indexSkippedRows = 0; string text = tileIndexCsv != null ? tileIndexCsv.text : LoadTextFromPath(); if (string.IsNullOrWhiteSpace(text)) { Debug.LogWarning("SweTileLoader: tile index CSV is empty."); indexLoaded = true; return; } string[] lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length <= 1) { indexLoaded = true; return; } int headerColumns = 0; for (int i = 0; i < lines.Length; i++) { string line = lines[i].Trim(); if (line.Length == 0) { continue; } string[] parts = line.Split(','); if (i == 0) { headerColumns = parts.Length; continue; } if (parts.Length < headerColumns) { indexSkippedRows++; continue; } if (parts.Length <= 11) { indexSkippedRows++; continue; } string lod = parts[0].Trim(); if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileX)) { indexSkippedRows++; continue; } if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileY)) { indexSkippedRows++; continue; } TileKey key = new TileKey(lod, tileX, tileY); records[key] = new TileRecord( lod, tileX, tileY, ParseFloat(parts[7], 1000.0f), ParseInt(parts[8], 256), NormalizePath(parts[9]), NormalizePath(parts[10]), NormalizePath(parts[11]) ); } indexRecordCount = records.Count; indexLoaded = true; if (verboseDiagnostics) { Debug.Log( $"SweTileLoader: tile index loaded (records={indexRecordCount}, skipped={indexSkippedRows}, " + $"source={DescribeIndexSource()})."); } } private string LoadTextFromPath() { if (string.IsNullOrWhiteSpace(tileIndexPath)) { return null; } if (sourceMode == SourceMode.Resources) { string resourcePath = ToResourcesPath(tileIndexPath); TextAsset resourceCsv = Resources.Load(resourcePath); if (resourceCsv != null) { return resourceCsv.text; } Debug.LogWarning($"SweTileLoader: Resources CSV not found at '{resourcePath}'."); } string path = tileIndexPath; if (sourceMode == SourceMode.StreamingAssets) { path = Path.Combine(Application.streamingAssetsPath, tileIndexPath); } if (!File.Exists(path)) { Debug.LogWarning($"SweTileLoader: CSV not found at {path}"); return null; } return File.ReadAllText(path); } private Texture2D LoadTexture(string path) { if (string.IsNullOrWhiteSpace(path)) { return null; } if (cacheTextures && textureCache.TryGetValue(path, out Texture2D cached)) { return cached; } Texture2D texture = null; switch (sourceMode) { case SourceMode.Resources: string resourcePath = ToResourcesPath(path); texture = Resources.Load(resourcePath); if (texture == null) { LogMissingTextureOnce(path, $"Resources/{resourcePath}"); } break; case SourceMode.StreamingAssets: case SourceMode.AbsolutePath: texture = LoadTextureFromFile(path); break; } if (cacheTextures && texture != null) { textureCache[path] = texture; } return texture; } private string NormalizePath(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return ""; } string path = raw.Trim().Replace("\\", "/"); if (sourceMode == SourceMode.AbsolutePath) { return path; } if (sourceMode == SourceMode.StreamingAssets) { return string.IsNullOrWhiteSpace(fileRoot) ? path : Path.Combine(fileRoot, path).Replace("\\", "/"); } return path; } private string ToResourcesPath(string path) { string normalized = path.Replace("\\", "/"); if (!string.IsNullOrWhiteSpace(resourcesRoot)) { int idx = normalized.IndexOf(resourcesRoot, StringComparison.OrdinalIgnoreCase); if (idx >= 0) { normalized = normalized.Substring(idx); } } if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) { normalized = normalized.Substring("Assets/".Length); } if (normalized.StartsWith("Resources/", StringComparison.OrdinalIgnoreCase)) { normalized = normalized.Substring("Resources/".Length); } int dot = normalized.LastIndexOf('.'); int slash = normalized.LastIndexOf('/'); if (dot > slash) { normalized = normalized.Substring(0, dot); } return normalized; } private Texture2D LoadTextureFromFile(string path) { string resolved = path; if (sourceMode == SourceMode.StreamingAssets) { resolved = Path.Combine(Application.streamingAssetsPath, path); } if (!File.Exists(resolved)) { LogMissingTextureOnce(path, resolved); return null; } byte[] bytes = File.ReadAllBytes(resolved); Texture2D texture = new Texture2D(2, 2, TextureFormat.RFloat, false, true); if (!ImageConversion.LoadImage(texture, bytes, false)) { Destroy(texture); Debug.LogWarning($"SweTileLoader: failed to decode {resolved}"); return null; } texture.wrapMode = TextureWrapMode.Clamp; texture.filterMode = FilterMode.Point; texture.name = Path.GetFileNameWithoutExtension(resolved); return texture; } private string BuildLoadSummary( string requestedTile, in TileRecord record, Texture2D height, Texture2D porosity, Texture2D buildings, Texture2D sourceIds, Texture2D sinkIds, string sourcePath, string sinkPath) { 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)) .Append("; sourceMask=").Append(DescribeTexture(sourceIds)) .Append(" (").Append(sourcePath).Append(")") .Append("; sinkMask=").Append(DescribeTexture(sinkIds)) .Append(" (").Append(sinkPath).Append(")") .Append("; loads=").Append(loadSuccesses).Append("/").Append(loadAttempts); return sb.ToString(); } private void LogMissingTextureOnce(string requestedPath, string resolvedPath) { if (!logMissingTextures) { return; } if (!missingTextureWarnings.Add(resolvedPath ?? requestedPath ?? "unknown")) { return; } Debug.LogWarning( $"SweTileLoader: texture not found (requested='{requestedPath}', resolved='{resolvedPath}', mode={sourceMode})."); } private string DescribeIndexSource() { if (tileIndexCsv != null) { return $"TextAsset:{tileIndexCsv.name}"; } if (sourceMode == SourceMode.StreamingAssets) { return $"StreamingAssets/{tileIndexPath}"; } if (sourceMode == SourceMode.AbsolutePath) { return tileIndexPath; } return $"Resources/{ToResourcesPath(tileIndexPath)}"; } private static string MakeTileKeyLabel(string lod, int x, int y) { string normalized = string.IsNullOrWhiteSpace(lod) ? "lod?" : lod.Trim(); return $"{normalized} ({x},{y})"; } private static string DescribeTexture(Texture2D texture) { if (texture == null) { return "missing"; } 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 { public readonly string Lod; public readonly int X; public readonly int Y; public TileKey(string lod, int x, int y) { Lod = lod ?? ""; X = x; Y = y; } public bool Equals(TileKey other) { return X == other.X && Y == other.Y && string.Equals(Lod, other.Lod, StringComparison.OrdinalIgnoreCase); } public override bool Equals(object obj) { return obj is TileKey other && Equals(other); } public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 31 + X; hash = hash * 31 + Y; hash = hash * 31 + StringComparer.OrdinalIgnoreCase.GetHashCode(Lod); return hash; } } } public readonly struct TileRecord { 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, 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; } } } public readonly struct SweTileData { 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; public readonly Texture2D SourceIds; public readonly Texture2D SinkIds; public SweTileData( in SweTileLoader.TileRecord record, Texture2D height, Texture2D porosity, Texture2D buildings, Texture2D sourceIds, Texture2D sinkIds) { Lod = record.Lod; TileX = record.X; TileY = record.Y; TileSizeM = record.TileSizeM; Resolution = record.Resolution; Height = height; Porosity = porosity; Buildings = buildings; SourceIds = sourceIds; SinkIds = sinkIds; } } }