Files
DTrierFlood_Linux/Assets/Scripts/Editor/GeoTileAddressablesBuilder.cs

497 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine;
public static class GeoTileAddressablesBuilder
{
private const string TilePrefabsDir = "Assets/TilePrefabs";
private const string TileIndexCsvPath = "Assets/GeoData/tile_index.csv";
private const string GroupName = "TilePrefabs";
private const string TileLabel = "tile";
private const string ManifestFileName = "TileManifest.json";
private const float DefaultTileSizeMeters = 1000f;
private const float DefaultTileSizeX = 1000f;
private const float DefaultTileSizeY = 1000f;
private const float DefaultOverlapX = 0.5f;
private const float DefaultOverlapY = 0.5f;
private const string BuildPathVariable = "TileBuildPath";
private const string LoadPathVariable = "TileLoadPath";
private const string BuildPathValue = "ServerData/TileBundles/[BuildTarget]";
private const string LoadPathValue = "file://{UnityEngine.Application.persistentDataPath}/TileBundles/[BuildTarget]";
internal struct TileKeyConfig
{
public float TileSizeX;
public float TileSizeY;
public float OverlapX;
public float OverlapY;
public static TileKeyConfig Default => new TileKeyConfig
{
TileSizeX = DefaultTileSizeX,
TileSizeY = DefaultTileSizeY,
OverlapX = DefaultOverlapX,
OverlapY = DefaultOverlapY
};
}
internal struct TileRecord
{
public string TileId;
public string TileKey;
public int XKey;
public int YKey;
public double Xmin;
public double Ymin;
public double GlobalMin;
public double GlobalMax;
}
internal struct BuildRequest
{
public string TileIndexCsvPath;
public string TilePrefabsDir;
public string OutputRoot;
public BuildTarget Target;
public BuildTargetGroup TargetGroup;
public float TileSizeMeters;
public TileKeyConfig TileKeyConfig;
public List<TileRecord> SelectedTiles;
public bool OverwriteExisting;
public bool Verbose;
}
[MenuItem("Tools/Geo Tiles/Build (Android)")]
public static void BuildAndroid()
{
Debug.LogWarning("[GeoTileAddressablesBuilder] Legacy build. Use Tools/Geo Tiles/Addressables Build Window for selection builds.");
BuildForTarget(BuildTarget.Android, BuildTargetGroup.Android);
}
[MenuItem("Tools/Geo Tiles/Build (Linux)")]
public static void BuildLinux()
{
Debug.LogWarning("[GeoTileAddressablesBuilder] Legacy build. Use Tools/Geo Tiles/Addressables Build Window for selection builds.");
BuildForTarget(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone);
}
private static void BuildForTarget(BuildTarget target, BuildTargetGroup group)
{
BuildSelectedTiles(new BuildRequest
{
TileIndexCsvPath = TileIndexCsvPath,
TilePrefabsDir = TilePrefabsDir,
OutputRoot = "ServerData/TileBundles",
Target = target,
TargetGroup = group,
TileSizeMeters = DefaultTileSizeMeters,
TileKeyConfig = TileKeyConfig.Default,
SelectedTiles = null,
OverwriteExisting = false,
Verbose = false
});
}
internal static bool BuildSelectedTiles(BuildRequest request)
{
var tileIndexCsvPath = string.IsNullOrWhiteSpace(request.TileIndexCsvPath) ? TileIndexCsvPath : request.TileIndexCsvPath;
var tilePrefabsDir = string.IsNullOrWhiteSpace(request.TilePrefabsDir) ? TilePrefabsDir : request.TilePrefabsDir;
var outputRoot = string.IsNullOrWhiteSpace(request.OutputRoot) ? "ServerData/TileBundles" : request.OutputRoot;
var tileSizeMeters = request.TileSizeMeters > 0f ? request.TileSizeMeters : DefaultTileSizeMeters;
var tileKeyConfig = request.TileKeyConfig;
if (tileKeyConfig.TileSizeX <= 0f && tileKeyConfig.TileSizeY <= 0f &&
tileKeyConfig.OverlapX == 0f && tileKeyConfig.OverlapY == 0f)
tileKeyConfig = TileKeyConfig.Default;
if (tileKeyConfig.TileSizeX <= 0f) tileKeyConfig.TileSizeX = DefaultTileSizeX;
if (tileKeyConfig.TileSizeY <= 0f) tileKeyConfig.TileSizeY = DefaultTileSizeY;
if (!Directory.Exists(tilePrefabsDir))
{
Debug.LogError($"[GeoTileAddressablesBuilder] Prefab directory missing: {tilePrefabsDir}");
return false;
}
if (!File.Exists(tileIndexCsvPath))
{
Debug.LogError($"[GeoTileAddressablesBuilder] CSV missing: {tileIndexCsvPath}");
return false;
}
var tiles = request.SelectedTiles ?? ParseTilesCsv(tileIndexCsvPath, tileKeyConfig);
if (tiles.Count == 0)
{
Debug.LogError("[GeoTileAddressablesBuilder] No tiles selected for build.");
return false;
}
var settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
if (settings == null)
{
Debug.LogError("[GeoTileAddressablesBuilder] Addressables settings not found.");
return false;
}
var buildPathValue = BuildPathFromOutputRoot(outputRoot);
EnsureProfileVariable(settings, BuildPathVariable, buildPathValue);
EnsureProfileVariable(settings, LoadPathVariable, LoadPathValue);
EnsureRemoteCatalogPaths(settings);
var groupAsset = GetOrCreateGroup(settings);
ConfigureGroup(settings, groupAsset);
var assignedTileIds = AssignPrefabs(settings, groupAsset, tilePrefabsDir, tiles, true);
var filteredTiles = tiles
.Where(tile => !string.IsNullOrWhiteSpace(tile.TileId) && assignedTileIds.Contains(tile.TileId))
.ToList();
if (filteredTiles.Count == 0)
{
Debug.LogError("[GeoTileAddressablesBuilder] No matching prefabs found for selected tiles.");
return false;
}
settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true);
AssetDatabase.SaveAssets();
var previousTarget = EditorUserBuildSettings.activeBuildTarget;
if (previousTarget != request.Target)
{
if (!EditorUserBuildSettings.SwitchActiveBuildTarget(request.TargetGroup, request.Target))
{
Debug.LogError($"[GeoTileAddressablesBuilder] Failed to switch build target to {request.Target}.");
return false;
}
}
var outputPath = GetRemoteCatalogBuildPath(settings, request.Target);
if (request.OverwriteExisting && Directory.Exists(outputPath))
Directory.Delete(outputPath, true);
AddressableAssetSettings.BuildPlayerContent();
outputPath = GetRemoteCatalogBuildPath(settings, request.Target);
if (!Directory.Exists(outputPath))
{
Debug.LogError($"[GeoTileAddressablesBuilder] Build output not found: {outputPath}");
return false;
}
var (catalogFile, catalogHashFile) = FindCatalogFiles(outputPath);
if (string.IsNullOrEmpty(catalogFile))
{
Debug.LogError($"[GeoTileAddressablesBuilder] Catalog file not found in: {outputPath}");
return false;
}
var manifest = BuildManifest(request.Target.ToString(), catalogFile, catalogHashFile, filteredTiles, tileSizeMeters);
var manifestPath = Path.Combine(outputPath, ManifestFileName);
File.WriteAllText(manifestPath, JsonUtility.ToJson(manifest, true));
AssetDatabase.Refresh();
if (request.Verbose)
Debug.Log($"[GeoTileAddressablesBuilder] Built {filteredTiles.Count} tiles (from {tiles.Count} selected). Output={outputPath}");
return true;
}
private static HashSet<string> AssignPrefabs(AddressableAssetSettings settings, AddressableAssetGroup group, string prefabsDir, List<TileRecord> tiles, bool removeUnselected)
{
var tileById = new Dictionary<string, TileRecord>(StringComparer.OrdinalIgnoreCase);
foreach (var tile in tiles)
{
if (!string.IsNullOrWhiteSpace(tile.TileId))
tileById[tile.TileId] = tile;
}
var assignedTileIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var selectedGuids = new HashSet<string>();
var prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { prefabsDir });
foreach (var guid in prefabGuids)
{
var path = AssetDatabase.GUIDToAssetPath(guid).Replace("\\", "/");
var parentDir = Path.GetDirectoryName(path)?.Replace("\\", "/");
if (!string.Equals(parentDir, prefabsDir, StringComparison.OrdinalIgnoreCase))
continue;
var tileId = Path.GetFileNameWithoutExtension(path);
if (!tileById.TryGetValue(tileId, out var tile))
continue;
var entry = settings.CreateOrMoveEntry(guid, group, false, false);
entry.address = tile.TileKey;
entry.SetLabel(TileLabel, true, true);
selectedGuids.Add(guid);
assignedTileIds.Add(tileId);
}
if (!removeUnselected)
return assignedTileIds;
var entries = group.entries.ToList();
foreach (var entry in entries)
{
if (entry == null || string.IsNullOrWhiteSpace(entry.AssetPath))
continue;
var entryPath = entry.AssetPath.Replace("\\", "/");
var entryDir = Path.GetDirectoryName(entryPath)?.Replace("\\", "/");
if (!string.Equals(entryDir, prefabsDir, StringComparison.OrdinalIgnoreCase))
continue;
if (!selectedGuids.Contains(entry.guid))
group.RemoveAssetEntry(entry);
}
return assignedTileIds;
}
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings)
{
var group = settings.FindGroup(GroupName);
if (group != null)
return group;
group = settings.CreateGroup(GroupName, false, false, false, new List<AddressableAssetGroupSchema>());
group.AddSchema<BundledAssetGroupSchema>();
group.AddSchema<ContentUpdateGroupSchema>();
return group;
}
private static void ConfigureGroup(AddressableAssetSettings settings, AddressableAssetGroup group)
{
var bundleSchema = group.GetSchema<BundledAssetGroupSchema>();
if (bundleSchema == null)
bundleSchema = group.AddSchema<BundledAssetGroupSchema>();
bundleSchema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackSeparately;
bundleSchema.BuildPath.SetVariableByName(settings, BuildPathVariable);
bundleSchema.LoadPath.SetVariableByName(settings, LoadPathVariable);
var updateSchema = group.GetSchema<ContentUpdateGroupSchema>();
if (updateSchema != null)
updateSchema.StaticContent = true;
}
private static void EnsureProfileVariable(AddressableAssetSettings settings, string name, string value)
{
var profileId = settings.activeProfileId;
var current = settings.profileSettings.GetValueByName(profileId, name);
if (string.IsNullOrEmpty(current))
settings.profileSettings.CreateValue(name, value);
settings.profileSettings.SetValue(profileId, name, value);
}
private static void EnsureRemoteCatalogPaths(AddressableAssetSettings settings)
{
settings.BuildRemoteCatalog = true;
settings.RemoteCatalogBuildPath.SetVariableByName(settings, BuildPathVariable);
settings.RemoteCatalogLoadPath.SetVariableByName(settings, LoadPathVariable);
}
private static (string catalogFile, string catalogHashFile) FindCatalogFiles(string outputPath)
{
var catalogFiles = Directory.GetFiles(outputPath, "*catalog*.json");
if (catalogFiles.Length == 0)
catalogFiles = Directory.GetFiles(outputPath, "*catalog*.bin");
var catalogFile = catalogFiles.Length > 0 ? Path.GetFileName(catalogFiles[0]) : null;
string catalogHash = null;
if (!string.IsNullOrEmpty(catalogFile))
{
var baseName = Path.GetFileNameWithoutExtension(catalogFile);
var hashCandidate = Path.Combine(outputPath, $"{baseName}.hash");
if (File.Exists(hashCandidate))
{
catalogHash = Path.GetFileName(hashCandidate);
}
else
{
var hashFiles = Directory.GetFiles(outputPath, "*catalog*.hash");
if (hashFiles.Length > 0)
catalogHash = Path.GetFileName(hashFiles[0]);
}
}
return (catalogFile, catalogHash);
}
private static TileManifest BuildManifest(string buildTarget, string catalogFile, string catalogHashFile, List<TileRecord> tiles, float tileSizeMeters)
{
if (tiles.Count == 0)
throw new InvalidOperationException("No tiles selected for TileManifest.");
double minX = double.PositiveInfinity;
double minY = double.PositiveInfinity;
foreach (var tile in tiles)
{
minX = Math.Min(minX, tile.Xmin);
minY = Math.Min(minY, tile.Ymin);
}
var entries = new TileEntry[tiles.Count];
for (int i = 0; i < tiles.Count; i++)
{
var tile = tiles[i];
entries[i] = new TileEntry
{
tileKey = tile.TileKey,
tileId = tile.TileId,
offsetX = (float)(tile.Xmin - minX),
offsetZ = (float)(tile.Ymin - minY),
baseY = (float)tile.GlobalMin
};
}
return new TileManifest
{
buildTarget = buildTarget,
catalogFile = catalogFile,
catalogHashFile = catalogHashFile,
tileSizeMeters = tileSizeMeters,
tiles = entries
};
}
private static string GetRemoteCatalogBuildPath(AddressableAssetSettings settings, BuildTarget target)
{
var rawPath = settings.RemoteCatalogBuildPath.GetValue(settings);
if (string.IsNullOrWhiteSpace(rawPath))
rawPath = BuildPathValue.Replace("[BuildTarget]", target.ToString());
return Path.GetFullPath(rawPath.Replace("[BuildTarget]", target.ToString()));
}
private static string BuildPathFromOutputRoot(string outputRoot)
{
if (string.IsNullOrWhiteSpace(outputRoot))
return BuildPathValue;
var normalized = outputRoot.Replace("\\", "/").TrimEnd('/');
if (normalized.IndexOf("[BuildTarget]", StringComparison.OrdinalIgnoreCase) >= 0)
return normalized;
return $"{normalized}/[BuildTarget]";
}
internal static List<TileRecord> ParseTilesCsv(string csvPath, TileKeyConfig tileKeyConfig)
{
var tiles = new List<TileRecord>();
var ci = CultureInfo.InvariantCulture;
var lines = File.ReadAllLines(csvPath);
if (lines.Length < 2)
return tiles;
var headerMap = BuildHeaderMap(lines[0]);
string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max" };
if (!HasAll(headerMap, required))
{
Debug.LogError("[GeoTileAddressablesBuilder] CSV missing required columns.");
return tiles;
}
int IDX_TILE = headerMap["tile_id"];
int IDX_XMIN = headerMap["xmin"];
int IDX_YMIN = headerMap["ymin"];
int IDX_GMIN = headerMap["global_min"];
int IDX_GMAX = headerMap["global_max"];
int IDX_TILE_KEY = headerMap.TryGetValue("tile_key", out var idxTileKey) ? idxTileKey : -1;
for (int i = 1; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrWhiteSpace(line))
continue;
var parts = line.Split(',');
int maxIdx = Math.Max(IDX_TILE, Math.Max(IDX_XMIN, Math.Max(IDX_YMIN, Math.Max(IDX_GMIN, IDX_GMAX))));
if (IDX_TILE_KEY >= 0)
maxIdx = Math.Max(maxIdx, IDX_TILE_KEY);
if (parts.Length <= maxIdx)
continue;
try
{
var xmin = double.Parse(parts[IDX_XMIN], ci);
var ymin = double.Parse(parts[IDX_YMIN], ci);
var tileKeyRaw = IDX_TILE_KEY >= 0 ? parts[IDX_TILE_KEY].Trim() : "";
var tileKey = ResolveTileKey(tileKeyRaw, xmin, ymin, tileKeyConfig, out var xKey, out var yKey);
tiles.Add(new TileRecord
{
TileId = parts[IDX_TILE].Trim(),
TileKey = tileKey,
XKey = xKey,
YKey = yKey,
Xmin = xmin,
Ymin = ymin,
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
GlobalMax = double.Parse(parts[IDX_GMAX], ci)
});
}
catch (Exception e)
{
Debug.LogWarning($"[GeoTileAddressablesBuilder] Parse error line {i + 1}: {e.Message}");
}
}
return tiles;
}
private static string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, TileKeyConfig tileKeyConfig, out int xKey, out int yKey)
{
if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey))
return tileKeyRaw;
float sizeX = tileKeyConfig.TileSizeX <= 0f ? 1f : tileKeyConfig.TileSizeX;
float sizeY = tileKeyConfig.TileSizeY <= 0f ? 1f : tileKeyConfig.TileSizeY;
xKey = (int)Math.Floor((xmin + tileKeyConfig.OverlapX) / sizeX);
yKey = (int)Math.Floor((ymin + tileKeyConfig.OverlapY) / sizeY);
return $"{xKey}_{yKey}";
}
private static bool TryParseTileKey(string tileKey, out int xKey, out int yKey)
{
xKey = 0;
yKey = 0;
if (string.IsNullOrWhiteSpace(tileKey))
return false;
var parts = tileKey.Split('_');
if (parts.Length != 2)
return false;
return int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out xKey)
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out yKey);
}
private static Dictionary<string, int> BuildHeaderMap(string headerLine)
{
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var headers = headerLine.Split(',');
for (int i = 0; i < headers.Length; i++)
map[headers[i].Trim()] = i;
return map;
}
private static bool HasAll(Dictionary<string, int> map, string[] keys)
{
foreach (var key in keys)
{
if (!map.ContainsKey(key))
return false;
}
return true;
}
}