From 04ef2c68ccb9b37ed8753d34fcba7a80ae5fa2a7 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Fri, 23 Jan 2026 16:07:24 +0100 Subject: [PATCH] add tile key selection for addressables builds --- .gitignore | 1 + .../AddressableAssetSettings.asset | 14 +- Assets/AddressableAssetsData/Linux.meta | 8 + .../Prefabs/XR Origin (XR Rig).prefab | 8 + .../Editor/GeoTileAddressablesBuilder.cs | 259 ++++++++-- .../Editor/GeoTileAddressablesWindow.cs | 375 +++++++++++++++ .../Editor/GeoTileAddressablesWindow.cs.meta | 2 + Assets/Scripts/Editor/GeoTileImporter.cs | 453 ++++++++++++++---- .../Scripts/Editor/GeoTilePrefabImporter.cs | 290 ++++++++++- Assets/Scripts/Editor/MissingScriptUtility.cs | 40 ++ .../GeoDataUtils/GeoTileAddressablesLoader.cs | 57 +-- .../Scripts/GeoDataUtils/GeoTileMetadata.cs | 1 + Assets/Scripts/GeoDataUtils/TileManifest.cs | 1 + Assets/Settings/PC_RPAsset.asset | 32 +- Packages/manifest.json | 2 + Packages/packages-lock.json | 25 + 16 files changed, 1383 insertions(+), 185 deletions(-) create mode 100644 Assets/AddressableAssetsData/Linux.meta create mode 100644 Assets/Scripts/Editor/GeoTileAddressablesWindow.cs create mode 100644 Assets/Scripts/Editor/GeoTileAddressablesWindow.cs.meta diff --git a/.gitignore b/.gitignore index a14cfb8..7657ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,4 @@ InitTestScene*.unity* # Auto-generated scenes by play mode tests /[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* +Notes diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index 4e95daa..7f65eb7 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -17,18 +17,18 @@ MonoBehaviour: serializedVersion: 2 Hash: 00000000000000000000000000000000 m_OptimizeCatalogSize: 0 - m_BuildRemoteCatalog: 0 + m_BuildRemoteCatalog: 1 m_CatalogRequestsTimeout: 0 m_DisableCatalogUpdateOnStart: 0 m_InternalIdNamingMode: 0 m_InternalBundleIdMode: 1 m_AssetLoadMode: 0 m_BundledAssetProviderType: - m_AssemblyName: - m_ClassName: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider m_AssetBundleProviderType: - m_AssemblyName: - m_ClassName: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider m_IgnoreUnsupportedFilesInBuild: 0 m_UniqueBundleIds: 0 m_EnableJsonCatalog: 0 @@ -51,9 +51,9 @@ MonoBehaviour: m_CheckForContentUpdateRestrictionsOption: 0 m_MonoScriptBundleCustomNaming: m_RemoteCatalogBuildPath: - m_Id: 888e852cf5299d044b396c8c5d1be8ca + m_Id: 3dbec98a2ceea7a2db5e98358038bd43 m_RemoteCatalogLoadPath: - m_Id: + m_Id: 334365b7569c7a681827d1a99c166327 m_ContentStateBuildPathProfileVariableName: m_CustomContentStateBuildPath: m_ContentStateBuildPath: diff --git a/Assets/AddressableAssetsData/Linux.meta b/Assets/AddressableAssetsData/Linux.meta new file mode 100644 index 0000000..6c6b6a1 --- /dev/null +++ b/Assets/AddressableAssetsData/Linux.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f929c00fb92096faea57ffc942e2424d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/XR Interaction Toolkit/3.3.1/Starter Assets/Prefabs/XR Origin (XR Rig).prefab b/Assets/Samples/XR Interaction Toolkit/3.3.1/Starter Assets/Prefabs/XR Origin (XR Rig).prefab index 5be04a9..a177936 100644 --- a/Assets/Samples/XR Interaction Toolkit/3.3.1/Starter Assets/Prefabs/XR Origin (XR Rig).prefab +++ b/Assets/Samples/XR Interaction Toolkit/3.3.1/Starter Assets/Prefabs/XR Origin (XR Rig).prefab @@ -1933,6 +1933,10 @@ PrefabInstance: propertyPath: m_Parameters.numCapVertices value: 4 objectReference: {fileID: 0} + - target: {fileID: 2761784063978902504, guid: c1800acf6366418a9b5f610249000331, type: 3} + propertyPath: m_Parameters.widthMultiplier + value: 0.01 + objectReference: {fileID: 0} - target: {fileID: 2761784063978902504, guid: c1800acf6366418a9b5f610249000331, type: 3} propertyPath: m_Parameters.numCornerVertices value: 4 @@ -2180,6 +2184,10 @@ PrefabInstance: propertyPath: m_Parameters.numCapVertices value: 4 objectReference: {fileID: 0} + - target: {fileID: 2761784063978902504, guid: c1800acf6366418a9b5f610249000331, type: 3} + propertyPath: m_Parameters.widthMultiplier + value: 0.01 + objectReference: {fileID: 0} - target: {fileID: 2761784063978902504, guid: c1800acf6366418a9b5f610249000331, type: 3} propertyPath: m_Parameters.numCornerVertices value: 4 diff --git a/Assets/Scripts/Editor/GeoTileAddressablesBuilder.cs b/Assets/Scripts/Editor/GeoTileAddressablesBuilder.cs index d7328bb..92d4c64 100644 --- a/Assets/Scripts/Editor/GeoTileAddressablesBuilder.cs +++ b/Assets/Scripts/Editor/GeoTileAddressablesBuilder.cs @@ -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 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 AssignPrefabs(AddressableAssetSettings settings, AddressableAssetGroup group, string prefabsDir, List tiles, bool removeUnselected) { - var prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { TilePrefabsDir }); + var tileById = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var tile in tiles) + { + if (!string.IsNullOrWhiteSpace(tile.TileId)) + tileById[tile.TileId] = tile; + } + + var assignedTileIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var selectedGuids = new HashSet(); + 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 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 ParseTilesCsv() + internal static List ParseTilesCsv(string csvPath, TileKeyConfig tileKeyConfig) { var tiles = new List(); 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 BuildHeaderMap(string headerLine) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs b/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs new file mode 100644 index 0000000..93536b8 --- /dev/null +++ b/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs @@ -0,0 +1,375 @@ +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; + } + } +} diff --git a/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs.meta b/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs.meta new file mode 100644 index 0000000..dd09cf7 --- /dev/null +++ b/Assets/Scripts/Editor/GeoTileAddressablesWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f9927ed702ab46b49721a4695f84efb7 diff --git a/Assets/Scripts/Editor/GeoTileImporter.cs b/Assets/Scripts/Editor/GeoTileImporter.cs index 482f7ac..66dbfbd 100644 --- a/Assets/Scripts/Editor/GeoTileImporter.cs +++ b/Assets/Scripts/Editor/GeoTileImporter.cs @@ -43,6 +43,24 @@ public class GeoTileImporter : EditorWindow private bool deleteExistingEnhancedTrees = false; private string enhancedTreesParentName = "Geo_Trees_Enhanced"; + private Vector2 scrollPosition; + private float tileKeySizeX = 1000f; + private float tileKeySizeY = 1000f; + private float tileKeyOverlapX = 0.5f; + private float tileKeyOverlapY = 0.5f; + private string bottomLeftKey = ""; + private string topRightKey = ""; + private readonly List tileIndexCache = new List(); + private readonly List tileKeyOptions = new List(); + private readonly Dictionary tileKeyLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + private string[] tileKeyStrings = Array.Empty(); + private string cachedTileCsvPath = ""; + private DateTime cachedTileCsvTimeUtc = DateTime.MinValue; + private float cachedTileKeySizeX = 0f; + private float cachedTileKeySizeY = 0f; + private float cachedTileOverlapX = 0f; + private float cachedTileOverlapY = 0f; + // Prefabs for trees and furniture (assign in editor) private GameObject treePrefab; private GameObject lampPrefab; @@ -60,6 +78,8 @@ public class GeoTileImporter : EditorWindow private void OnGUI() { + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + GUILayout.Label("Inputs", EditorStyles.boldLabel); tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath); heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir); @@ -110,6 +130,19 @@ public class GeoTileImporter : EditorWindow bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false); defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture Prefab", defaultFurniturePrefab, typeof(GameObject), false); + GUILayout.Space(12); + GUILayout.Label("Tile Key Config", EditorStyles.boldLabel); + tileKeySizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileKeySizeX); + tileKeySizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileKeySizeY); + tileKeyOverlapX = EditorGUILayout.FloatField("Overlap X (m)", tileKeyOverlapX); + tileKeyOverlapY = EditorGUILayout.FloatField("Overlap Y (m)", tileKeyOverlapY); + + RefreshTileIndexCache(); + + GUILayout.Space(10); + GUILayout.Label("Tile Selection", EditorStyles.boldLabel); + DrawTileSelectionUI(); + GUILayout.Space(12); if (GUILayout.Button("Import / Rebuild")) ImportTiles(); @@ -119,6 +152,8 @@ public class GeoTileImporter : EditorWindow "Absolute elevation mapping: Terrain Y = global_min, Terrain height = (global_max - global_min).\n" + "CSV is header-driven (order-independent). Optionally applies ortho JPGs and instantiates buildings/trees GLBs.", MessageType.Info); + + EditorGUILayout.EndScrollView(); } private static void EnsureHeightmapImportSettings(string assetPath) @@ -258,6 +293,8 @@ public class GeoTileImporter : EditorWindow applyOrthoTextures = false; } + RefreshTileIndexCache(); + var parent = GameObject.Find(parentName); if (parent == null) parent = new GameObject(parentName); @@ -267,118 +304,51 @@ public class GeoTileImporter : EditorWindow DestroyImmediate(parent.transform.GetChild(i).gameObject); } - var ci = CultureInfo.InvariantCulture; - var lines = File.ReadAllLines(tilesCsvPath); - - Debug.Log($"[GeoTileImporter] Read {lines.Length} lines."); - if (lines.Length < 2) + var tiles = ParseTilesCsv(); + if (tiles == null || tiles.Count == 0) { - Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row)."); + Debug.LogError("[GeoTileImporter] No valid tiles found in CSV."); return; } - var headerLine = lines[0].Trim(); - var headerMap = BuildHeaderMap(headerLine); - Debug.Log($"[GeoTileImporter] Header: {headerLine}"); - Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}"); - - // Required columns (order-independent) - string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" }; - if (!HasAll(headerMap, required)) + var selectedTiles = ApplySelection(tiles); + if (selectedTiles.Count == 0) { - Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " + - string.Join(", ", required) + - "\nFound: " + string.Join(", ", headerMap.Keys)); + Debug.LogError("[GeoTileImporter] Selection is empty. Adjust Tile Selection and try again."); return; } - 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_RES = headerMap["out_res"]; - - // Compute origin from min xmin/ymin double originX = double.PositiveInfinity; double originY = double.PositiveInfinity; - - int validRowsForOrigin = 0; - for (int i = 1; i < lines.Length; i++) + for (int i = 0; i < selectedTiles.Count; i++) { - var line = lines[i].Trim(); - if (string.IsNullOrWhiteSpace(line)) continue; - - var parts = line.Split(','); - // Robust: just ensure indices exist in this row - int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES)); - if (parts.Length <= needMaxIndex) - { - Debug.LogWarning($"[GeoTileImporter] Origin scan: skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'"); - continue; - } - - try - { - double xmin = double.Parse(parts[IDX_XMIN], ci); - double ymin = double.Parse(parts[IDX_YMIN], ci); - originX = Math.Min(originX, xmin); - originY = Math.Min(originY, ymin); - validRowsForOrigin++; - } - catch (Exception e) - { - Debug.LogWarning($"[GeoTileImporter] Origin scan parse failed line {i + 1}: '{line}'\n{e.Message}"); - } + originX = Math.Min(originX, selectedTiles[i].Xmin); + originY = Math.Min(originY, selectedTiles[i].Ymin); } - if (validRowsForOrigin == 0 || double.IsInfinity(originX) || double.IsInfinity(originY)) + if (double.IsInfinity(originX) || double.IsInfinity(originY)) { Debug.LogError("[GeoTileImporter] Could not compute origin (no valid rows parsed). Check CSV numeric format."); return; } - Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {validRowsForOrigin} valid rows."); + Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {selectedTiles.Count} selected tiles."); int imported = 0, skipped = 0; int importedTextures = 0; var placements = new List<(string tileId, float ux, float uz, float gmin)>(); - for (int i = 1; i < lines.Length; i++) + for (int i = 0; i < selectedTiles.Count; i++) { - var line = lines[i].Trim(); - if (string.IsNullOrWhiteSpace(line)) continue; + var tile = selectedTiles[i]; + var tileId = tile.TileId; + double xmin = tile.Xmin; + double ymin = tile.Ymin; + double gmin = tile.GlobalMin; + double gmax = tile.GlobalMax; - var parts = line.Split(','); - int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES)); - if (parts.Length <= needMaxIndex) - { - skipped++; - Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'"); - continue; - } - - string tileId = parts[IDX_TILE].Trim(); - - double xmin, ymin, gmin, gmax; - int outRes; - try - { - xmin = double.Parse(parts[IDX_XMIN], ci); - ymin = double.Parse(parts[IDX_YMIN], ci); - gmin = double.Parse(parts[IDX_GMIN], ci); - gmax = double.Parse(parts[IDX_GMAX], ci); - outRes = int.Parse(parts[IDX_RES], ci); - } - catch (Exception e) - { - skipped++; - Debug.LogWarning($"[GeoTileImporter] Parse failed line {i + 1} tile '{tileId}': {e.Message}\nLine: '{line}'"); - continue; - } - - if (outRes != heightmapResolution) - Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={outRes} but importer expects {heightmapResolution}."); + if (tile.OutRes != heightmapResolution) + Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={tile.OutRes} but importer expects {heightmapResolution}."); float heightRange = (float)(gmax - gmin); if (heightRange <= 0.0001f) @@ -493,7 +463,7 @@ public class GeoTileImporter : EditorWindow } } - Debug.Log($"[GeoTileImporter] Imported {tileId} @ XZ=({ux},{uz}) Y={gmin} heightRange={heightRange} usedU16={usedU16}"); + Debug.Log($"[GeoTileImporter] Imported {tileId} ({tile.TileKey}) @ XZ=({ux},{uz}) Y={gmin} heightRange={heightRange} usedU16={usedU16}"); imported++; placements.Add((tileId, ux, uz, (float)gmin)); } @@ -509,6 +479,313 @@ public class GeoTileImporter : EditorWindow ImportEnhancedTrees(placements); } + private void RefreshTileIndexCache() + { + if (!File.Exists(tilesCsvPath)) + { + tileIndexCache.Clear(); + tileKeyOptions.Clear(); + tileKeyLookup.Clear(); + tileKeyStrings = Array.Empty(); + return; + } + + var writeTime = File.GetLastWriteTimeUtc(tilesCsvPath); + if (tilesCsvPath == cachedTileCsvPath && + writeTime == cachedTileCsvTimeUtc && + Mathf.Approximately(tileKeySizeX, cachedTileKeySizeX) && + Mathf.Approximately(tileKeySizeY, cachedTileKeySizeY) && + Mathf.Approximately(tileKeyOverlapX, cachedTileOverlapX) && + Mathf.Approximately(tileKeyOverlapY, cachedTileOverlapY)) + { + return; + } + + tileIndexCache.Clear(); + tileIndexCache.AddRange(ParseTilesCsv()); + BuildTileKeyOptions(); + + cachedTileCsvPath = tilesCsvPath; + cachedTileCsvTimeUtc = writeTime; + cachedTileKeySizeX = tileKeySizeX; + cachedTileKeySizeY = tileKeySizeY; + cachedTileOverlapX = tileKeyOverlapX; + cachedTileOverlapY = tileKeyOverlapY; + } + + private void BuildTileKeyOptions() + { + tileKeyOptions.Clear(); + tileKeyLookup.Clear(); + + for (int i = 0; i < tileIndexCache.Count; i++) + { + var tile = tileIndexCache[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 DrawTileSelectionUI() + { + 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 < tileIndexCache.Count; i++) + { + var tile = tileIndexCache[i]; + if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && + tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) + count++; + } + return count; + } + + private List ApplySelection(List tiles) + { + if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) || + !tileKeyLookup.TryGetValue(topRightKey, out var topRight)) + return tiles; + + var selected = new List(); + for (int i = 0; i < tiles.Count; i++) + { + var tile = tiles[i]; + if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && + tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) + selected.Add(tile); + } + return selected; + } + + private List ParseTilesCsv() + { + var tiles = new List(); + var ci = CultureInfo.InvariantCulture; + var lines = File.ReadAllLines(tilesCsvPath); + + if (lines.Length < 2) + { + Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row)."); + return tiles; + } + + var headerLine = lines[0].Trim(); + var headerMap = BuildHeaderMap(headerLine); + Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}"); + + string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" }; + if (!HasAll(headerMap, required)) + { + Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " + + string.Join(", ", required) + + "\nFound: " + string.Join(", ", headerMap.Keys)); + 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_RES = headerMap["out_res"]; + int IDX_TILE_KEY = headerMap.ContainsKey("tile_key") ? headerMap["tile_key"] : -1; + + for (int i = 1; i < lines.Length; i++) + { + var line = lines[i].Trim(); + if (string.IsNullOrWhiteSpace(line)) + continue; + + var parts = line.Split(','); + int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES)); + if (IDX_TILE_KEY >= 0) + needMaxIndex = Math.Max(needMaxIndex, IDX_TILE_KEY); + + if (parts.Length <= needMaxIndex) + { + Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length})."); + 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, 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), + OutRes = int.Parse(parts[IDX_RES], ci) + }); + } + catch (Exception e) + { + Debug.LogWarning($"[GeoTileImporter] Parse error line {i + 1}: {e.Message}"); + } + } + + return tiles; + } + + private string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, out int xKey, out int yKey) + { + if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey)) + return tileKeyRaw; + + float sizeX = tileKeySizeX <= 0f ? 1f : tileKeySizeX; + float sizeY = tileKeySizeY <= 0f ? 1f : tileKeySizeY; + xKey = (int)Math.Floor((xmin + tileKeyOverlapX) / sizeX); + yKey = (int)Math.Floor((ymin + tileKeyOverlapY) / 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 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 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; + public int OutRes; + } + + private struct TileKeyOption + { + public string Key; + public int X; + public int Y; + } + + private void ImportBuildings(List<(string tileId, float ux, float uz, float gmin)> placements) { if (!importBuildings) diff --git a/Assets/Scripts/Editor/GeoTilePrefabImporter.cs b/Assets/Scripts/Editor/GeoTilePrefabImporter.cs index 514ee7e..6860e94 100644 --- a/Assets/Scripts/Editor/GeoTilePrefabImporter.cs +++ b/Assets/Scripts/Editor/GeoTilePrefabImporter.cs @@ -35,6 +35,24 @@ public class GeoTilePrefabImporter : EditorWindow private bool includeFurniture = false; private bool includeEnhancedTrees = false; + private Vector2 scrollPosition; + private float tileKeySizeX = 1000f; + private float tileKeySizeY = 1000f; + private float tileKeyOverlapX = 0.5f; + private float tileKeyOverlapY = 0.5f; + private string bottomLeftKey = ""; + private string topRightKey = ""; + private readonly List tileIndexCache = new List(); + private readonly List tileKeyOptions = new List(); + private readonly Dictionary tileKeyLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + private string[] tileKeyStrings = Array.Empty(); + private string cachedTileCsvPath = ""; + private DateTime cachedTileCsvTimeUtc = DateTime.MinValue; + private float cachedTileKeySizeX = 0f; + private float cachedTileKeySizeY = 0f; + private float cachedTileOverlapX = 0f; + private float cachedTileOverlapY = 0f; + // Prefabs for furniture (optional) private GameObject lampPrefab; private GameObject benchPrefab; @@ -45,7 +63,10 @@ public class GeoTilePrefabImporter : EditorWindow private struct TileMetadata { + public string TileKey; public string TileId; + public int XKey; + public int YKey; public double Xmin, Ymin, Xmax, Ymax; public double GlobalMin, GlobalMax; public int OutRes; @@ -60,6 +81,8 @@ public class GeoTilePrefabImporter : EditorWindow private void OnGUI() { + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + GUILayout.Label("Input Paths", EditorStyles.boldLabel); tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath); heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir); @@ -96,6 +119,19 @@ public class GeoTilePrefabImporter : EditorWindow bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false); defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture", defaultFurniturePrefab, typeof(GameObject), false); + GUILayout.Space(12); + GUILayout.Label("Tile Key Config", EditorStyles.boldLabel); + tileKeySizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileKeySizeX); + tileKeySizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileKeySizeY); + tileKeyOverlapX = EditorGUILayout.FloatField("Overlap X (m)", tileKeyOverlapX); + tileKeyOverlapY = EditorGUILayout.FloatField("Overlap Y (m)", tileKeyOverlapY); + + RefreshTileIndexCache(); + + GUILayout.Space(10); + GUILayout.Label("Tile Selection", EditorStyles.boldLabel); + DrawTileSelectionUI(); + GUILayout.Space(15); if (GUILayout.Button("Generate Prefabs")) ImportTilesAsPrefabs(); @@ -110,6 +146,8 @@ public class GeoTilePrefabImporter : EditorWindow "TerrainData and TerrainLayers are saved as separate .asset files.\n" + "IMPORTANT: Use 'Place All Prefabs in Scene' to position tiles correctly.", MessageType.Info); + + EditorGUILayout.EndScrollView(); } private void PlaceAllPrefabsInScene() @@ -211,6 +249,7 @@ public class GeoTilePrefabImporter : EditorWindow EnsureDirectoryExists($"{prefabOutputDir}/TerrainLayers"); // Parse CSV + RefreshTileIndexCache(); var tiles = ParseTilesCsv(); if (tiles == null || tiles.Count == 0) { @@ -218,17 +257,24 @@ public class GeoTilePrefabImporter : EditorWindow return; } - Debug.Log($"[GeoTilePrefabImporter] Found {tiles.Count} tiles to process."); + var selectedTiles = ApplySelection(tiles); + if (selectedTiles.Count == 0) + { + Debug.LogError("[GeoTilePrefabImporter] Selection is empty. Adjust Tile Selection and try again."); + return; + } + + Debug.Log($"[GeoTilePrefabImporter] Found {selectedTiles.Count} tiles to process."); int created = 0, skipped = 0, failed = 0; - for (int i = 0; i < tiles.Count; i++) + for (int i = 0; i < selectedTiles.Count; i++) { - var tile = tiles[i]; + var tile = selectedTiles[i]; EditorUtility.DisplayProgressBar( "Creating Tile Prefabs", - $"Processing {tile.TileId} ({i + 1}/{tiles.Count})", - (float)i / tiles.Count); + $"Processing {tile.TileId} ({i + 1}/{selectedTiles.Count})", + (float)i / selectedTiles.Count); string prefabPath = $"{prefabOutputDir}/{tile.TileId}.prefab"; if (File.Exists(prefabPath) && !overwriteExisting) @@ -259,6 +305,168 @@ public class GeoTilePrefabImporter : EditorWindow Debug.Log($"[GeoTilePrefabImporter] DONE. Created={created}, Skipped={skipped}, Failed={failed}"); } + private void RefreshTileIndexCache() + { + if (!File.Exists(tilesCsvPath)) + { + tileIndexCache.Clear(); + tileKeyOptions.Clear(); + tileKeyLookup.Clear(); + tileKeyStrings = Array.Empty(); + return; + } + + var writeTime = File.GetLastWriteTimeUtc(tilesCsvPath); + if (tilesCsvPath == cachedTileCsvPath && + writeTime == cachedTileCsvTimeUtc && + Mathf.Approximately(tileKeySizeX, cachedTileKeySizeX) && + Mathf.Approximately(tileKeySizeY, cachedTileKeySizeY) && + Mathf.Approximately(tileKeyOverlapX, cachedTileOverlapX) && + Mathf.Approximately(tileKeyOverlapY, cachedTileOverlapY)) + { + return; + } + + tileIndexCache.Clear(); + tileIndexCache.AddRange(ParseTilesCsv()); + BuildTileKeyOptions(); + + cachedTileCsvPath = tilesCsvPath; + cachedTileCsvTimeUtc = writeTime; + cachedTileKeySizeX = tileKeySizeX; + cachedTileKeySizeY = tileKeySizeY; + cachedTileOverlapX = tileKeyOverlapX; + cachedTileOverlapY = tileKeyOverlapY; + } + + private void BuildTileKeyOptions() + { + tileKeyOptions.Clear(); + tileKeyLookup.Clear(); + + for (int i = 0; i < tileIndexCache.Count; i++) + { + var tile = tileIndexCache[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 DrawTileSelectionUI() + { + 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 < tileIndexCache.Count; i++) + { + var tile = tileIndexCache[i]; + if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && + tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) + count++; + } + return count; + } + + private List ApplySelection(List tiles) + { + if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) || + !tileKeyLookup.TryGetValue(topRightKey, out var topRight)) + return tiles; + + var selected = new List(); + for (int i = 0; i < tiles.Count; i++) + { + var tile = tiles[i]; + if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X && + tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y) + selected.Add(tile); + } + return selected; + } + private List ParseTilesCsv() { var tiles = new List(); @@ -288,6 +496,7 @@ public class GeoTilePrefabImporter : EditorWindow int IDX_GMIN = headerMap["global_min"]; int IDX_GMAX = headerMap["global_max"]; int IDX_RES = headerMap["out_res"]; + int IDX_TILE_KEY = headerMap.ContainsKey("tile_key") ? headerMap["tile_key"] : -1; for (int i = 1; i < lines.Length; i++) { @@ -295,7 +504,11 @@ public class GeoTilePrefabImporter : EditorWindow if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(','); - int maxIdx = Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMAX), IDX_YMAX), Math.Max(IDX_GMAX, IDX_RES)); + int maxIdx = Math.Max( + Math.Max(Math.Max(IDX_TILE, IDX_XMIN), Math.Max(IDX_YMIN, IDX_XMAX)), + Math.Max(Math.Max(IDX_YMAX, IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES))); + if (IDX_TILE_KEY >= 0) + maxIdx = Math.Max(maxIdx, IDX_TILE_KEY); if (parts.Length <= maxIdx) { Debug.LogWarning($"[GeoTilePrefabImporter] Skipping line {i + 1}: too few columns."); @@ -304,11 +517,19 @@ public class GeoTilePrefabImporter : EditorWindow 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, out var xKey, out var yKey); + tiles.Add(new TileMetadata { + TileKey = tileKey, TileId = parts[IDX_TILE].Trim(), - Xmin = double.Parse(parts[IDX_XMIN], ci), - Ymin = double.Parse(parts[IDX_YMIN], ci), + XKey = xKey, + YKey = yKey, + Xmin = xmin, + Ymin = ymin, Xmax = double.Parse(parts[IDX_XMAX], ci), Ymax = double.Parse(parts[IDX_YMAX], ci), GlobalMin = double.Parse(parts[IDX_GMIN], ci), @@ -325,6 +546,58 @@ public class GeoTilePrefabImporter : EditorWindow return tiles; } + private string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, out int xKey, out int yKey) + { + if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey)) + return tileKeyRaw; + + float sizeX = tileKeySizeX <= 0f ? 1f : tileKeySizeX; + float sizeY = tileKeySizeY <= 0f ? 1f : tileKeySizeY; + xKey = (int)Math.Floor((xmin + tileKeyOverlapX) / sizeX); + yKey = (int)Math.Floor((ymin + tileKeyOverlapY) / 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 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 struct TileKeyOption + { + public string Key; + public int X; + public int Y; + } + private bool CreateTilePrefab(TileMetadata tile) { // Validate height range @@ -407,6 +680,7 @@ public class GeoTilePrefabImporter : EditorWindow // Store metadata as component for later use var metadata = root.AddComponent(); + metadata.tileKey = tile.TileKey; metadata.tileId = tile.TileId; metadata.xmin = tile.Xmin; metadata.ymin = tile.Ymin; diff --git a/Assets/Scripts/Editor/MissingScriptUtility.cs b/Assets/Scripts/Editor/MissingScriptUtility.cs index 8f1721f..0f97eb1 100644 --- a/Assets/Scripts/Editor/MissingScriptUtility.cs +++ b/Assets/Scripts/Editor/MissingScriptUtility.cs @@ -1,5 +1,7 @@ #if UNITY_EDITOR +using System; using System.Collections.Generic; +using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; @@ -46,6 +48,44 @@ public static class MissingScriptUtility Debug.Log($"[MissingScriptUtility] Missing scripts found in prefabs: {missingCount}"); } + [MenuItem("Tools/Diagnostics/Find Missing Scripts in ScriptableObjects")] + public static void FindMissingScriptsInScriptableObjects() + { + var assetGuids = AssetDatabase.FindAssets("t:ScriptableObject"); + int missingCount = 0; + + for (int i = 0; i < assetGuids.Length; i++) + { + var path = AssetDatabase.GUIDToAssetPath(assetGuids[i]); + EditorUtility.DisplayProgressBar("Scanning ScriptableObjects", path, (float)i / assetGuids.Length); + + if (string.IsNullOrWhiteSpace(path) || !path.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) + continue; + + if (!File.Exists(path)) + continue; + + string contents; + try + { + contents = File.ReadAllText(path); + } + catch + { + continue; + } + + if (contents.Contains("m_Script: {fileID: 0}")) + { + missingCount++; + Debug.LogWarning($"[MissingScriptUtility] Missing script reference in asset: {path}"); + } + } + + EditorUtility.ClearProgressBar(); + Debug.Log($"[MissingScriptUtility] Missing scripts found in scriptable objects: {missingCount}"); + } + [MenuItem("Tools/Diagnostics/Remove Missing Scripts in Open Scenes")] public static void RemoveMissingScriptsInOpenScenes() { diff --git a/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs b/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs index d5106bc..21828bd 100644 --- a/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs +++ b/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs @@ -89,8 +89,9 @@ public class GeoTileAddressablesLoader : MonoBehaviour tiles.Clear(); foreach (var tile in manifest.tiles) { - if (!string.IsNullOrWhiteSpace(tile.tileId)) - tiles[tile.tileId] = tile; + var key = !string.IsNullOrWhiteSpace(tile.tileKey) ? tile.tileKey : tile.tileId; + if (!string.IsNullOrWhiteSpace(key)) + tiles[key] = tile; } Log($"Manifest loaded. Tiles={tiles.Count} CatalogFile={manifest.catalogFile}"); @@ -149,69 +150,69 @@ public class GeoTileAddressablesLoader : MonoBehaviour if (distance <= loadRadiusMeters) { - EnqueueTileLoad(tile.tileId); + EnqueueTileLoad(kvp.Key); } else if (distance >= unloadRadiusMeters) { - UnloadTile(tile.tileId); + UnloadTile(kvp.Key); } } } - private void EnqueueTileLoad(string tileId) + private void EnqueueTileLoad(string tileKey) { - if (loaded.ContainsKey(tileId) || loading.Contains(tileId) || queued.Contains(tileId)) + if (loaded.ContainsKey(tileKey) || loading.Contains(tileKey) || queued.Contains(tileKey)) return; - loadQueue.Enqueue(tileId); - queued.Add(tileId); + loadQueue.Enqueue(tileKey); + queued.Add(tileKey); } private void ProcessQueue() { while (loading.Count < maxConcurrentLoads && loadQueue.Count > 0) { - var tileId = loadQueue.Dequeue(); - queued.Remove(tileId); - StartLoad(tileId); + var tileKey = loadQueue.Dequeue(); + queued.Remove(tileKey); + StartLoad(tileKey); } } - private void StartLoad(string tileId) + private void StartLoad(string tileKey) { - if (!tiles.TryGetValue(tileId, out var tile)) + if (!tiles.TryGetValue(tileKey, out var tile)) return; - loading.Add(tileId); - Log($"Loading tile {tileId}..."); + loading.Add(tileKey); + Log($"Loading tile {tileKey}..."); - var handle = Addressables.InstantiateAsync(tileId, tilesParent); + var handle = Addressables.InstantiateAsync(tileKey, tilesParent); handle.Completed += op => { - loading.Remove(tileId); + loading.Remove(tileKey); if (op.Status != AsyncOperationStatus.Succeeded) { - Debug.LogError($"[GeoTileAddressablesLoader] Load failed for {tileId}: {op.OperationException}"); + Debug.LogError($"[GeoTileAddressablesLoader] Load failed for {tileKey}: {op.OperationException}"); return; } var instance = op.Result; - instance.name = tileId; + instance.name = tileKey; instance.transform.position = new Vector3(tile.offsetX, tile.baseY, tile.offsetZ); - loaded[tileId] = instance; - Log($"Loaded tile {tileId}. LoadedCount={loaded.Count}"); + loaded[tileKey] = instance; + Log($"Loaded tile {tileKey}. LoadedCount={loaded.Count}"); }; } - private void UnloadTile(string tileId) + private void UnloadTile(string tileKey) { - if (!loaded.TryGetValue(tileId, out var instance)) + if (!loaded.TryGetValue(tileKey, out var instance)) return; Addressables.ReleaseInstance(instance); - loaded.Remove(tileId); - Log($"Unloaded tile {tileId}. LoadedCount={loaded.Count}"); + loaded.Remove(tileKey); + Log($"Unloaded tile {tileKey}. LoadedCount={loaded.Count}"); } private static string GetBuildTargetFolderName() @@ -222,13 +223,13 @@ public class GeoTileAddressablesLoader : MonoBehaviour return "Android"; case RuntimePlatform.WindowsPlayer: case RuntimePlatform.WindowsEditor: - return "Windows"; + return "StandaloneWindows64"; case RuntimePlatform.LinuxPlayer: case RuntimePlatform.LinuxEditor: - return "Linux"; + return "StandaloneLinux64"; case RuntimePlatform.OSXPlayer: case RuntimePlatform.OSXEditor: - return "OSX"; + return "StandaloneOSX"; default: return "Android"; } diff --git a/Assets/Scripts/GeoDataUtils/GeoTileMetadata.cs b/Assets/Scripts/GeoDataUtils/GeoTileMetadata.cs index e48e676..b871bf2 100644 --- a/Assets/Scripts/GeoDataUtils/GeoTileMetadata.cs +++ b/Assets/Scripts/GeoDataUtils/GeoTileMetadata.cs @@ -6,6 +6,7 @@ using UnityEngine; /// public class GeoTileMetadata : MonoBehaviour { + public string tileKey; public string tileId; public double xmin; public double ymin; diff --git a/Assets/Scripts/GeoDataUtils/TileManifest.cs b/Assets/Scripts/GeoDataUtils/TileManifest.cs index aac3afe..9551ed9 100644 --- a/Assets/Scripts/GeoDataUtils/TileManifest.cs +++ b/Assets/Scripts/GeoDataUtils/TileManifest.cs @@ -13,6 +13,7 @@ public class TileManifest [Serializable] public class TileEntry { + public string tileKey; public string tileId; public float offsetX; public float offsetZ; diff --git a/Assets/Settings/PC_RPAsset.asset b/Assets/Settings/PC_RPAsset.asset index 9b2b046..97ee4bb 100644 --- a/Assets/Settings/PC_RPAsset.asset +++ b/Assets/Settings/PC_RPAsset.asset @@ -101,38 +101,38 @@ MonoBehaviour: m_Keys: [] m_Values: m_PrefilteringModeMainLightShadows: 3 - m_PrefilteringModeAdditionalLight: 4 - m_PrefilteringModeAdditionalLightShadows: 0 + m_PrefilteringModeAdditionalLight: 0 + m_PrefilteringModeAdditionalLightShadows: 2 m_PrefilterXRKeywords: 1 - m_PrefilteringModeForwardPlus: 1 + m_PrefilteringModeForwardPlus: 2 m_PrefilteringModeDeferredRendering: 0 - m_PrefilteringModeScreenSpaceOcclusion: 1 + m_PrefilteringModeScreenSpaceOcclusion: 0 m_PrefilterDebugKeywords: 1 - m_PrefilterWriteRenderingLayers: 0 + m_PrefilterWriteRenderingLayers: 1 m_PrefilterHDROutput: 1 - m_PrefilterAlphaOutput: 0 - m_PrefilterSSAODepthNormals: 0 + m_PrefilterAlphaOutput: 1 + m_PrefilterSSAODepthNormals: 1 m_PrefilterSSAOSourceDepthLow: 1 m_PrefilterSSAOSourceDepthMedium: 1 m_PrefilterSSAOSourceDepthHigh: 1 m_PrefilterSSAOInterleaved: 1 - m_PrefilterSSAOBlueNoise: 0 + m_PrefilterSSAOBlueNoise: 1 m_PrefilterSSAOSampleCountLow: 1 - m_PrefilterSSAOSampleCountMedium: 0 + m_PrefilterSSAOSampleCountMedium: 1 m_PrefilterSSAOSampleCountHigh: 1 m_PrefilterDBufferMRT1: 1 m_PrefilterDBufferMRT2: 1 - m_PrefilterDBufferMRT3: 0 - m_PrefilterSoftShadowsQualityLow: 0 - m_PrefilterSoftShadowsQualityMedium: 0 - m_PrefilterSoftShadowsQualityHigh: 0 + m_PrefilterDBufferMRT3: 1 + m_PrefilterSoftShadowsQualityLow: 1 + m_PrefilterSoftShadowsQualityMedium: 1 + m_PrefilterSoftShadowsQualityHigh: 1 m_PrefilterSoftShadows: 0 m_PrefilterScreenCoord: 1 - m_PrefilterScreenSpaceIrradiance: 0 + m_PrefilterScreenSpaceIrradiance: 1 m_PrefilterNativeRenderPass: 1 m_PrefilterUseLegacyLightmaps: 0 - m_PrefilterBicubicLightmapSampling: 0 - m_PrefilterReflectionProbeRotation: 0 + m_PrefilterBicubicLightmapSampling: 1 + m_PrefilterReflectionProbeRotation: 1 m_PrefilterReflectionProbeBlending: 0 m_PrefilterReflectionProbeBoxProjection: 0 m_PrefilterReflectionProbeAtlas: 0 diff --git a/Packages/manifest.json b/Packages/manifest.json index e70d206..19839d8 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -11,8 +11,10 @@ "com.unity.inputsystem": "1.17.0", "com.unity.multiplayer.center": "1.0.1", "com.unity.render-pipelines.universal": "17.3.0", + "com.unity.sdk.linux-x86_64": "1.0.2", "com.unity.test-framework": "1.6.0", "com.unity.timeline": "1.8.10", + "com.unity.toolchain.linux-x86_64-linux": "1.0.2", "com.unity.ugui": "2.0.0", "com.unity.visualscripting": "1.9.9", "com.unity.xr.arfoundation": "6.3.2", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index cdc4af7..9cd4a1a 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -264,6 +264,15 @@ }, "url": "https://packages.unity.com" }, + "com.unity.sdk.linux-x86_64": { + "version": "1.0.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.sysroot.base": "1.0.2" + }, + "url": "https://packages.unity.com" + }, "com.unity.searcher": { "version": "4.9.4", "depth": 2, @@ -287,6 +296,13 @@ "com.unity.searcher": "4.9.3" } }, + "com.unity.sysroot.base": { + "version": "1.0.2", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -319,6 +335,15 @@ }, "url": "https://packages.unity.com" }, + "com.unity.toolchain.linux-x86_64-linux": { + "version": "1.0.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.sysroot.base": "1.0.2" + }, + "url": "https://packages.unity.com" + }, "com.unity.ugui": { "version": "2.0.0", "depth": 0,