Files
DTrierFlood_New/Assets/FloodSWE/Scripts/IO/SweTileLoader.cs

558 lines
18 KiB
C#

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<TileKey, TileRecord> records = new Dictionary<TileKey, TileRecord>();
private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> missingTextureWarnings = new HashSet<string>(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.sourceIdMask = data.SourceIds;
simulator.sinkIdMask = data.SinkIds;
if (verboseDiagnostics)
{
Debug.Log(
$"SweTileLoader: applied tile {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
$"to simulator (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,
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<TextAsset>(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<Texture2D>(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("; 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 readonly struct TileKey : IEquatable<TileKey>
{
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 string HeightPath;
public readonly string PorosityPath;
public readonly string BuildingPath;
public TileRecord(string lod, int x, int y, string heightPath, string porosityPath, string buildingPath)
{
Lod = lod;
X = x;
Y = y;
HeightPath = heightPath;
PorosityPath = porosityPath;
BuildingPath = buildingPath;
}
}
}
public readonly struct SweTileData
{
public readonly string Lod;
public readonly int TileX;
public readonly int TileY;
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;
Height = height;
Porosity = porosity;
Buildings = buildings;
SourceIds = sourceIds;
SinkIds = sinkIds;
}
}
}