#if UNITY_EDITOR using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Unity.Entities; using Unity.Scenes; using UnityEditor; using UnityEngine; namespace AutomaticDoorSystem.Editor { /// /// Answers "which door ids does each subscene AUTHOR, and which ids actually EXIST as baked /// entities right now" - the two sides whose difference is a stale bake, a bake-skipped door, /// or a leftover entity. Loaded subscenes are read from their live objects; closed ones are /// text-scanned (serialized DoorAuthoring blocks plus prefab instances, resolving prefab /// default ids and per-instance overrides) so nothing has to be opened to be checked. /// public static class SubSceneDoorScanner { public class SubSceneDoors { public SubScene SubScene; public string ScenePath; public bool IsLoaded; public List AuthoringIds = new(); /// Ids whose authoring GameObject is inactive (only known for loaded /// subscenes - text scans cannot cheaply resolve activity). public HashSet InactiveAuthoringIds = new(); } private static readonly Regex PlainDoorId = new(@"^ doorId: (\d+)\r?$", RegexOptions.Multiline | RegexOptions.Compiled); private static readonly Regex OverrideDoorId = new(@"propertyPath: doorId\s*\r?\n\s*value: (\d+)\r?$", RegexOptions.Multiline | RegexOptions.Compiled); private static readonly Regex SourcePrefabGuid = new(@"m_SourcePrefab: \{fileID: \d+, guid: ([0-9a-f]{32})", RegexOptions.Compiled); // Prefab guid -> default door ids of the DoorAuthorings inside it (empty when none). private static readonly Dictionary PrefabDoorIdCache = new(); /// Authored door ids for every SubScene component in the open hierarchy. public static List ScanAll() { var results = new List(); foreach (var subScene in Object.FindObjectsByType(FindObjectsInactive.Include)) { if (subScene.SceneAsset == null) continue; var entry = new SubSceneDoors { SubScene = subScene, ScenePath = AssetDatabase.GetAssetPath(subScene.SceneAsset), IsLoaded = subScene.IsLoaded }; if (subScene.IsLoaded && subScene.EditingScene.IsValid()) { foreach (var root in subScene.EditingScene.GetRootGameObjects()) { foreach (var door in root.GetComponentsInChildren(true)) { entry.AuthoringIds.Add(door.doorId); if (!door.gameObject.activeInHierarchy) { entry.InactiveAuthoringIds.Add(door.doorId); } } } } else { entry.AuthoringIds.AddRange(ScanSceneFile(entry.ScenePath)); } results.Add(entry); } return results; } /// /// Door ids authored in a scene file without opening it: plain serialized DoorAuthoring /// blocks, plus prefab instances of prefabs that contain DoorAuthoring (per-instance /// doorId overrides when present, the prefab's default ids otherwise). /// public static List ScanSceneFile(string scenePath) { var ids = new List(); if (string.IsNullOrEmpty(scenePath)) return ids; string fullPath = Path.GetFullPath(scenePath); if (!File.Exists(fullPath)) return ids; var text = File.ReadAllText(fullPath); // Plain (non-prefab) DoorAuthoring components serialized straight into the scene. foreach (Match m in PlainDoorId.Matches(text)) { ids.Add(int.Parse(m.Groups[1].Value)); } // Prefab instances: "--- !u!1001" blocks. Within a block, doorId overrides win; doors // in the prefab beyond the overridden count contribute their prefab-default id. var blocks = text.Split(new[] { "--- !u!1001 " }, System.StringSplitOptions.None); for (int i = 1; i < blocks.Length; i++) { var block = blocks[i]; var guidMatch = SourcePrefabGuid.Match(block); if (!guidMatch.Success) continue; var defaults = GetPrefabDoorIds(guidMatch.Groups[1].Value); if (defaults.Length == 0) continue; var overrides = OverrideDoorId.Matches(block).Select(m2 => int.Parse(m2.Groups[1].Value)).ToList(); ids.AddRange(overrides); // Remaining doors in the prefab keep their default id. (Matching each override to // a specific door inside a multi-door prefab is not attempted - door prefabs here // carry one DoorAuthoring each, and the count-based fallback stays correct for those.) for (int d = overrides.Count; d < defaults.Length; d++) { ids.Add(defaults[d]); } } return ids; } private static int[] GetPrefabDoorIds(string guid) { if (PrefabDoorIdCache.TryGetValue(guid, out var cached)) return cached; int[] result = System.Array.Empty(); var path = AssetDatabase.GUIDToAssetPath(guid); if (!string.IsNullOrEmpty(path)) { var prefab = AssetDatabase.LoadAssetAtPath(path); if (prefab != null) { result = prefab.GetComponentsInChildren(true).Select(d => d.doorId).ToArray(); } } PrefabDoorIdCache[guid] = result; return result; } /// /// Door ids that exist as baked entities in the default world right now - closed subscenes /// stream their baked entities into the editor world, open ones are live-baked, so in both /// edit and play mode this is "what the runtime would actually see". Doors baked from /// inactive GameObjects carry the Disabled tag and land in : /// they exist in the bake but are invisible to detection, animation and audio until /// something activates them - the correct state for a deliberately disabled door variant, /// and the explanation when a "configured" door does nothing at runtime. /// public static void BakedDoorIds(out HashSet enabled, out HashSet disabled) { enabled = new HashSet(); disabled = new HashSet(); var world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) return; using (var query = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly())) using (var doors = query.ToComponentDataArray(Unity.Collections.Allocator.Temp)) { foreach (var door in doors) enabled.Add(door.DoorId); } var allDesc = new EntityQueryDesc { All = new[] { ComponentType.ReadOnly() }, Options = EntityQueryOptions.IncludeDisabledEntities }; using (var query = world.EntityManager.CreateEntityQuery(allDesc)) using (var doors = query.ToComponentDataArray(Unity.Collections.Allocator.Temp)) { foreach (var door in doors) { if (!enabled.Contains(door.DoorId)) disabled.Add(door.DoorId); } } } /// Call when assets may have changed (the cache holds prefab door ids). public static void ClearCache() => PrefabDoorIdCache.Clear(); } } #endif