using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; public class GeoTileAddressablesWindow : EditorWindow { private string tileIndexCsvPath = "Assets/GeoData/tile_index.csv"; private string tilePrefabsDir = "Assets/TilePrefabs"; private string outputRoot = "ServerData/TileBundles"; private BuildTarget buildTarget = BuildTarget.StandaloneLinux64; private float tileSizeX = 1000f; private float tileSizeY = 1000f; private float overlapX = 0.5f; private float overlapY = 0.5f; private string bottomLeftKey = ""; private string topRightKey = ""; private bool overwriteExisting = true; private bool verboseLogging = false; private Vector2 scrollPosition; private enum BuildSource { Prefabs, SceneTerrains } private BuildSource buildSource = BuildSource.Prefabs; private readonly List tileRecords = new List(); private readonly List tileKeyOptions = new List(); private readonly Dictionary tileKeyLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); private string[] tileKeyStrings = Array.Empty(); private string cachedCsvPath = ""; private DateTime cachedCsvWriteTimeUtc = DateTime.MinValue; private float cachedTileSizeX = 0f; private float cachedTileSizeY = 0f; private float cachedOverlapX = 0f; private float cachedOverlapY = 0f; private struct TileKeyOption { public string Key; public int X; public int Y; } [MenuItem("Tools/Geo Tiles/Addressables Build Window")] public static void ShowWindow() { var win = GetWindow("Geo Tile Addressables Build"); win.minSize = new Vector2(620, 420); } private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); GUILayout.Label("Geo Tile Addressables Build", EditorStyles.boldLabel); GUILayout.Space(8); GUILayout.Label("Paths", EditorStyles.boldLabel); tileIndexCsvPath = EditorGUILayout.TextField("Tile Index CSV", tileIndexCsvPath); tilePrefabsDir = EditorGUILayout.TextField("Tile Prefabs Dir", tilePrefabsDir); outputRoot = EditorGUILayout.TextField("Output Root", outputRoot); buildTarget = (BuildTarget)EditorGUILayout.EnumPopup("Build Target", buildTarget); GUILayout.Space(10); GUILayout.Label("Tile Key Config", EditorStyles.boldLabel); tileSizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileSizeX); tileSizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileSizeY); overlapX = EditorGUILayout.FloatField("Overlap X (m)", overlapX); overlapY = EditorGUILayout.FloatField("Overlap Y (m)", overlapY); RefreshTileCache(); GUILayout.Space(10); GUILayout.Label("Tile Selection", EditorStyles.boldLabel); DrawTileSelection(); GUILayout.Space(10); GUILayout.Label("Build Options", EditorStyles.boldLabel); buildSource = (BuildSource)EditorGUILayout.EnumPopup("Source", buildSource); overwriteExisting = EditorGUILayout.ToggleLeft("Overwrite existing bundles", overwriteExisting); verboseLogging = EditorGUILayout.ToggleLeft("Verbose logging", verboseLogging); if (buildSource == BuildSource.SceneTerrains) EditorGUILayout.HelpBox("Scene terrain source is not implemented yet.", MessageType.Info); GUILayout.Space(12); if (GUILayout.Button("Build Addressables (Selected Tiles)")) BuildSelectedTiles(); if (GUILayout.Button("Open Output Folder")) OpenOutputFolder(); EditorGUILayout.EndScrollView(); } private void RefreshTileCache() { if (!File.Exists(tileIndexCsvPath)) { tileRecords.Clear(); tileKeyOptions.Clear(); tileKeyLookup.Clear(); tileKeyStrings = Array.Empty(); return; } var writeTime = File.GetLastWriteTimeUtc(tileIndexCsvPath); if (tileIndexCsvPath == cachedCsvPath && writeTime == cachedCsvWriteTimeUtc && Mathf.Approximately(tileSizeX, cachedTileSizeX) && Mathf.Approximately(tileSizeY, cachedTileSizeY) && Mathf.Approximately(overlapX, cachedOverlapX) && Mathf.Approximately(overlapY, cachedOverlapY)) { return; } var config = new GeoTileAddressablesBuilder.TileKeyConfig { TileSizeX = tileSizeX, TileSizeY = tileSizeY, OverlapX = overlapX, OverlapY = overlapY }; tileRecords.Clear(); tileRecords.AddRange(GeoTileAddressablesBuilder.ParseTilesCsv(tileIndexCsvPath, config)); BuildTileKeyOptions(); cachedCsvPath = tileIndexCsvPath; cachedCsvWriteTimeUtc = writeTime; cachedTileSizeX = tileSizeX; cachedTileSizeY = tileSizeY; cachedOverlapX = overlapX; cachedOverlapY = overlapY; } private void BuildTileKeyOptions() { tileKeyOptions.Clear(); tileKeyLookup.Clear(); for (int i = 0; i < tileRecords.Count; i++) { var tile = tileRecords[i]; if (string.IsNullOrWhiteSpace(tile.TileKey)) continue; if (tileKeyLookup.ContainsKey(tile.TileKey)) continue; var option = new TileKeyOption { Key = tile.TileKey, X = tile.XKey, Y = tile.YKey }; tileKeyOptions.Add(option); tileKeyLookup[tile.TileKey] = option; } tileKeyOptions.Sort((a, b) => { int cmp = a.X.CompareTo(b.X); return cmp != 0 ? cmp : a.Y.CompareTo(b.Y); }); tileKeyStrings = new string[tileKeyOptions.Count]; for (int i = 0; i < tileKeyOptions.Count; i++) tileKeyStrings[i] = tileKeyOptions[i].Key; } private void DrawTileSelection() { if (tileKeyOptions.Count == 0) { EditorGUILayout.HelpBox("No tile keys available. Check the tile index CSV and key config.", MessageType.Info); EditorGUILayout.LabelField("Tiles in selection", "0"); return; } if (string.IsNullOrEmpty(bottomLeftKey) || !tileKeyLookup.ContainsKey(bottomLeftKey)) bottomLeftKey = tileKeyOptions[0].Key; var bottomLeft = tileKeyLookup[bottomLeftKey]; var topRightOptions = GetTopRightOptions(bottomLeft); if (topRightOptions.Count == 0) topRightOptions.Add(bottomLeft); if (string.IsNullOrEmpty(topRightKey) || !ContainsKey(topRightOptions, topRightKey)) topRightKey = topRightOptions[topRightOptions.Count - 1].Key; int bottomLeftIndex = Array.IndexOf(tileKeyStrings, bottomLeftKey); int newBottomLeftIndex = EditorGUILayout.Popup("Bottom-Left Key", bottomLeftIndex, tileKeyStrings); if (newBottomLeftIndex != bottomLeftIndex) { bottomLeftKey = tileKeyStrings[newBottomLeftIndex]; bottomLeft = tileKeyLookup[bottomLeftKey]; topRightOptions = GetTopRightOptions(bottomLeft); if (topRightOptions.Count == 0) topRightOptions.Add(bottomLeft); if (!ContainsKey(topRightOptions, topRightKey)) topRightKey = topRightOptions[topRightOptions.Count - 1].Key; } var topRightKeys = BuildKeyArray(topRightOptions); int topRightIndex = Array.IndexOf(topRightKeys, topRightKey); if (topRightIndex < 0) topRightIndex = topRightKeys.Length - 1; int newTopRightIndex = EditorGUILayout.Popup("Top-Right Key", topRightIndex, topRightKeys); if (newTopRightIndex != topRightIndex) topRightKey = topRightKeys[newTopRightIndex]; EditorGUILayout.LabelField("Tiles in selection", CountSelectedTiles().ToString()); } private List GetTopRightOptions(TileKeyOption bottomLeft) { var options = new List(); for (int i = 0; i < tileKeyOptions.Count; i++) { var option = tileKeyOptions[i]; if (option.X >= bottomLeft.X && option.Y >= bottomLeft.Y) options.Add(option); } return options; } private int CountSelectedTiles() { if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) || !tileKeyLookup.TryGetValue(topRightKey, out var topRight)) return 0; int count = 0; for (int i = 0; i < tileRecords.Count; i++) { var tile = tileRecords[i]; if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) count++; } return count; } private List GetSelectedTiles() { var selected = new List(); if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) || !tileKeyLookup.TryGetValue(topRightKey, out var topRight)) return selected; for (int i = 0; i < tileRecords.Count; i++) { var tile = tileRecords[i]; if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) selected.Add(tile); } return selected; } private void BuildSelectedTiles() { if (buildSource != BuildSource.Prefabs) { Debug.LogError("[GeoTileAddressablesWindow] Scene terrain source is not implemented."); return; } RefreshTileCache(); if (tileRecords.Count == 0) { Debug.LogError("[GeoTileAddressablesWindow] No tiles loaded from CSV."); return; } var selectedTiles = GetSelectedTiles(); if (selectedTiles.Count == 0) { Debug.LogError("[GeoTileAddressablesWindow] Selection is empty."); return; } var request = new GeoTileAddressablesBuilder.BuildRequest { TileIndexCsvPath = tileIndexCsvPath, TilePrefabsDir = tilePrefabsDir, OutputRoot = outputRoot, Target = buildTarget, TargetGroup = GetBuildTargetGroup(buildTarget), TileSizeMeters = tileSizeX, TileKeyConfig = new GeoTileAddressablesBuilder.TileKeyConfig { TileSizeX = tileSizeX, TileSizeY = tileSizeY, OverlapX = overlapX, OverlapY = overlapY }, SelectedTiles = selectedTiles, OverwriteExisting = overwriteExisting, Verbose = verboseLogging }; GeoTileAddressablesBuilder.BuildSelectedTiles(request); } private void OpenOutputFolder() { if (string.IsNullOrWhiteSpace(outputRoot)) return; var normalized = outputRoot.Replace("\\", "/").TrimEnd('/'); var resolved = normalized.IndexOf("[BuildTarget]", StringComparison.OrdinalIgnoreCase) >= 0 ? normalized.Replace("[BuildTarget]", buildTarget.ToString()) : $"{normalized}/{buildTarget}"; var fullPath = Path.GetFullPath(resolved); if (Directory.Exists(fullPath)) EditorUtility.RevealInFinder(fullPath); else Debug.LogWarning($"[GeoTileAddressablesWindow] Output folder not found: {fullPath}"); } private static bool ContainsKey(List options, string key) { for (int i = 0; i < options.Count; i++) { if (string.Equals(options[i].Key, key, StringComparison.OrdinalIgnoreCase)) return true; } return false; } private static string[] BuildKeyArray(List options) { var keys = new string[options.Count]; for (int i = 0; i < options.Count; i++) keys[i] = options[i].Key; return keys; } private static BuildTargetGroup GetBuildTargetGroup(BuildTarget target) { switch (target) { case BuildTarget.Android: return BuildTargetGroup.Android; case BuildTarget.StandaloneLinux64: case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: case BuildTarget.StandaloneOSX: return BuildTargetGroup.Standalone; case BuildTarget.iOS: return BuildTargetGroup.iOS; case BuildTarget.WebGL: return BuildTargetGroup.WebGL; default: return BuildTargetGroup.Standalone; } } }