add tile key selection for addressables builds
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
@@ -15,99 +16,240 @@ public static class GeoTileAddressablesBuilder
|
||||
private const string GroupName = "TilePrefabs";
|
||||
private const string TileLabel = "tile";
|
||||
private const string ManifestFileName = "TileManifest.json";
|
||||
private const float TileSizeMeters = 1000f;
|
||||
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]";
|
||||
|
||||
[MenuItem("Tools/Geo Tiles/Build Tile Addressables (Android)")]
|
||||
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)
|
||||
{
|
||||
if (!Directory.Exists(TilePrefabsDir))
|
||||
BuildSelectedTiles(new BuildRequest
|
||||
{
|
||||
Debug.LogError($"[GeoTileAddressablesBuilder] Prefab directory missing: {TilePrefabsDir}");
|
||||
return;
|
||||
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))
|
||||
if (!File.Exists(tileIndexCsvPath))
|
||||
{
|
||||
Debug.LogError($"[GeoTileAddressablesBuilder] CSV missing: {TileIndexCsvPath}");
|
||||
return;
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureProfileVariable(settings, BuildPathVariable, BuildPathValue);
|
||||
var buildPathValue = BuildPathFromOutputRoot(outputRoot);
|
||||
EnsureProfileVariable(settings, BuildPathVariable, buildPathValue);
|
||||
EnsureProfileVariable(settings, LoadPathVariable, LoadPathValue);
|
||||
EnsureRemoteCatalogPaths(settings);
|
||||
|
||||
var groupAsset = GetOrCreateGroup(settings);
|
||||
ConfigureGroup(settings, groupAsset);
|
||||
AssignPrefabs(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 != target)
|
||||
if (previousTarget != request.Target)
|
||||
{
|
||||
if (!EditorUserBuildSettings.SwitchActiveBuildTarget(group, target))
|
||||
if (!EditorUserBuildSettings.SwitchActiveBuildTarget(request.TargetGroup, request.Target))
|
||||
{
|
||||
Debug.LogError($"[GeoTileAddressablesBuilder] Failed to switch build target to {target}.");
|
||||
return;
|
||||
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();
|
||||
|
||||
var outputPath = GetRemoteCatalogBuildPath(settings, target);
|
||||
outputPath = GetRemoteCatalogBuildPath(settings, request.Target);
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Debug.LogError($"[GeoTileAddressablesBuilder] Build output not found: {outputPath}");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (catalogFile, catalogHashFile) = FindCatalogFiles(outputPath);
|
||||
if (string.IsNullOrEmpty(catalogFile))
|
||||
{
|
||||
Debug.LogError($"[GeoTileAddressablesBuilder] Catalog file not found in: {outputPath}");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var manifest = BuildManifest(target.ToString(), catalogFile, catalogHashFile);
|
||||
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();
|
||||
|
||||
Debug.Log($"[GeoTileAddressablesBuilder] DONE. Output={outputPath}");
|
||||
if (request.Verbose)
|
||||
Debug.Log($"[GeoTileAddressablesBuilder] Built {filteredTiles.Count} tiles (from {tiles.Count} selected). Output={outputPath}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AssignPrefabs(AddressableAssetSettings settings, AddressableAssetGroup group)
|
||||
private static HashSet<string> AssignPrefabs(AddressableAssetSettings settings, AddressableAssetGroup group, string prefabsDir, List<TileRecord> tiles, bool removeUnselected)
|
||||
{
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { TilePrefabsDir });
|
||||
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, TilePrefabsDir, StringComparison.OrdinalIgnoreCase))
|
||||
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 = Path.GetFileNameWithoutExtension(path);
|
||||
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)
|
||||
@@ -182,11 +324,10 @@ public static class GeoTileAddressablesBuilder
|
||||
return (catalogFile, catalogHash);
|
||||
}
|
||||
|
||||
private static TileManifest BuildManifest(string buildTarget, string catalogFile, string catalogHashFile)
|
||||
private static TileManifest BuildManifest(string buildTarget, string catalogFile, string catalogHashFile, List<TileRecord> tiles, float tileSizeMeters)
|
||||
{
|
||||
var tiles = ParseTilesCsv();
|
||||
if (tiles.Count == 0)
|
||||
throw new InvalidOperationException("No tiles parsed from tile_index.csv");
|
||||
throw new InvalidOperationException("No tiles selected for TileManifest.");
|
||||
|
||||
double minX = double.PositiveInfinity;
|
||||
double minY = double.PositiveInfinity;
|
||||
@@ -203,6 +344,7 @@ public static class GeoTileAddressablesBuilder
|
||||
var tile = tiles[i];
|
||||
entries[i] = new TileEntry
|
||||
{
|
||||
tileKey = tile.TileKey,
|
||||
tileId = tile.TileId,
|
||||
offsetX = (float)(tile.Xmin - minX),
|
||||
offsetZ = (float)(tile.Ymin - minY),
|
||||
@@ -215,7 +357,7 @@ public static class GeoTileAddressablesBuilder
|
||||
buildTarget = buildTarget,
|
||||
catalogFile = catalogFile,
|
||||
catalogHashFile = catalogHashFile,
|
||||
tileSizeMeters = TileSizeMeters,
|
||||
tileSizeMeters = tileSizeMeters,
|
||||
tiles = entries
|
||||
};
|
||||
}
|
||||
@@ -229,20 +371,23 @@ public static class GeoTileAddressablesBuilder
|
||||
return Path.GetFullPath(rawPath.Replace("[BuildTarget]", target.ToString()));
|
||||
}
|
||||
|
||||
private struct TileRecord
|
||||
private static string BuildPathFromOutputRoot(string outputRoot)
|
||||
{
|
||||
public string TileId;
|
||||
public double Xmin;
|
||||
public double Ymin;
|
||||
public double GlobalMin;
|
||||
public double GlobalMax;
|
||||
if (string.IsNullOrWhiteSpace(outputRoot))
|
||||
return BuildPathValue;
|
||||
|
||||
var normalized = outputRoot.Replace("\\", "/").TrimEnd('/');
|
||||
if (normalized.IndexOf("[BuildTarget]", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return normalized;
|
||||
|
||||
return $"{normalized}/[BuildTarget]";
|
||||
}
|
||||
|
||||
private static List<TileRecord> ParseTilesCsv()
|
||||
internal static List<TileRecord> ParseTilesCsv(string csvPath, TileKeyConfig tileKeyConfig)
|
||||
{
|
||||
var tiles = new List<TileRecord>();
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
var lines = File.ReadAllLines(TileIndexCsvPath);
|
||||
var lines = File.ReadAllLines(csvPath);
|
||||
|
||||
if (lines.Length < 2)
|
||||
return tiles;
|
||||
@@ -260,6 +405,7 @@ public static class GeoTileAddressablesBuilder
|
||||
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++)
|
||||
{
|
||||
@@ -269,16 +415,26 @@ public static class GeoTileAddressablesBuilder
|
||||
|
||||
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(),
|
||||
Xmin = double.Parse(parts[IDX_XMIN], ci),
|
||||
Ymin = double.Parse(parts[IDX_YMIN], ci),
|
||||
TileKey = tileKey,
|
||||
XKey = xKey,
|
||||
YKey = yKey,
|
||||
Xmin = xmin,
|
||||
Ymin = ymin,
|
||||
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
|
||||
GlobalMax = double.Parse(parts[IDX_GMAX], ci)
|
||||
});
|
||||
@@ -292,6 +448,33 @@ public static class GeoTileAddressablesBuilder
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user