#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using AutomaticDoorSystem.Utilities; using Unity.Scenes; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using Object = UnityEngine.Object; namespace AutomaticDoorSystem.Editor { /// /// Checks that the two halves of a door setup line up: the main scene (pool managers + bridges, /// which are plain MonoBehaviours) and the world subscene (DoorAuthoring + the player's /// DoorTriggerableAuthoring, which only exist as entities once baked). /// Also creates the ScriptableObjects and the pooled AudioSource prefab a project needs. /// public class DoorSetupValidatorWindow : EditorWindow { private enum Severity { Ok, Info, Warning, Error } private class Check { public Severity Severity; public string Message; public Object Context; public string FixLabel; public Action Fix; } private readonly List<(string Title, List Checks)> _sections = new(); private Vector2 _scroll; private bool _hasRun; [MenuItem("Tools/Jeanf/AutomaticDoorSystem/Setup Validator")] public static void Open() { var window = GetWindow("Door Setup"); window.minSize = new Vector2(460f, 400f); window.RunChecks(); window.Show(); } private void OnFocus() { if (_hasRun) RunChecks(); } #region GUI private void OnGUI() { using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { if (GUILayout.Button("Re-run checks", EditorStyles.toolbarButton, GUILayout.Width(110f))) { RunChecks(); } GUILayout.FlexibleSpace(); if (_hasRun) { int errors = CountOf(Severity.Error); int warnings = CountOf(Severity.Warning); GUILayout.Label(errors == 0 && warnings == 0 ? "No problems found" : $"{errors} error(s), {warnings} warning(s)", EditorStyles.miniLabel); } } _scroll = EditorGUILayout.BeginScrollView(_scroll); DrawAssetCreation(); foreach (var (title, checks) in _sections) { EditorGUILayout.Space(); EditorGUILayout.LabelField(title, EditorStyles.boldLabel); foreach (var check in checks) { DrawCheck(check); } } EditorGUILayout.Space(); EditorGUILayout.EndScrollView(); } private void DrawCheck(Check check) { using (new EditorGUILayout.VerticalScope()) { EditorGUILayout.HelpBox(check.Message, ToMessageType(check.Severity)); bool hasContext = check.Context != null; bool hasFix = check.Fix != null; if (!hasContext && !hasFix) return; using (new EditorGUILayout.HorizontalScope()) { if (hasContext && GUILayout.Button("Select", GUILayout.Width(60f))) { Selection.activeObject = check.Context; EditorGUIUtility.PingObject(check.Context); } GUILayout.FlexibleSpace(); if (hasFix && GUILayout.Button(check.FixLabel ?? "Fix", GUILayout.Width(220f))) { check.Fix(); RunChecks(); GUIUtility.ExitGUI(); } } } } private void DrawAssetCreation() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Create configuration assets", EditorStyles.boldLabel); using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField( "Each button asks where to save. Skip the ones you already have set up in this project.", EditorStyles.wordWrappedMiniLabel); if (GUILayout.Button("Create DoorConfig assets (all 4 door types)...")) { CreateDoorConfigSet(); RunChecks(); GUIUtility.ExitGUI(); } if (GUILayout.Button("Create DoorAudioConfiguration asset...")) { CreateDoorAudioConfiguration(); RunChecks(); GUIUtility.ExitGUI(); } if (GUILayout.Button("Create pooled AudioSource prefab...")) { CreateAudioSourcePrefab(); RunChecks(); GUIUtility.ExitGUI(); } } } private static MessageType ToMessageType(Severity severity) { return severity switch { Severity.Error => MessageType.Error, Severity.Warning => MessageType.Warning, Severity.Info => MessageType.Info, _ => MessageType.None }; } private int CountOf(Severity severity) { return _sections.Sum(section => section.Checks.Count(c => c.Severity == severity)); } #endregion #region Checks private void RunChecks() { _sections.Clear(); _hasRun = true; var subScenePaths = GetOpenSubScenePaths(); var doors = FindAll(); CheckManagers(subScenePaths); CheckPlayer(doors, subScenePaths); CheckDoors(doors, subScenePaths); CheckSubSceneBakes(); CheckAudioConfigurations(doors); CheckLegacyIdentifiers(doors); Repaint(); } private void CheckManagers(HashSet subScenePaths) { var checks = new List(); var colliderPools = FindAll(); var audioPools = FindAll(); var dataBridges = FindAll(); var audioBridges = FindAll(); bool nothingAtAll = colliderPools.Count == 0 && audioPools.Count == 0 && dataBridges.Count == 0 && audioBridges.Count == 0; if (nothingAtAll) { checks.Add(new Check { Severity = Severity.Error, Message = "No door management components in the loaded scenes. The pooled colliders, " + "pooled AudioSources and the ECS<->MonoBehaviour bridges all live in the main scene.", FixLabel = "Create DoorManagement GameObject", Fix = CreateDoorManagement }); } else { var host = colliderPools.FirstOrDefault()?.gameObject ?? audioPools.FirstOrDefault()?.gameObject ?? dataBridges.FirstOrDefault()?.gameObject ?? audioBridges.FirstOrDefault()?.gameObject; AddManagerCheck(checks, colliderPools, host, subScenePaths, "pooled BoxColliders that follow the animated door panels"); AddManagerCheck(checks, audioPools, host, subScenePaths, "pooled AudioSources for door sounds"); AddManagerCheck(checks, dataBridges, host, subScenePaths, "door positions and states read from the entities"); AddManagerCheck(checks, audioBridges, host, subScenePaths, "audio events routed from the entities to the pooled AudioSources"); } var audioPool = audioPools.FirstOrDefault(); if (audioPool != null && audioPool.audioSourcePrefab == null) { checks.Add(new Check { Severity = Severity.Info, Message = "AudioSourcePoolManager has no AudioSource prefab. Plain AudioSources will be " + "created instead, which means no Steam Audio spatialization on door sounds.", Context = audioPool, FixLabel = "Create and assign prefab...", Fix = () => CreateAudioSourcePrefab(audioPool) }); } if (Camera.main == null) { checks.Add(new Check { Severity = Severity.Warning, Message = "No enabled camera tagged MainCamera in the loaded scenes. Both pools cull by " + "distance to Camera.main, and the player follower entity tracks it too - without " + "it no door will ever activate. (Fine if your player rig is spawned at runtime.)" }); } AddSection("Main scene", checks); } private void AddManagerCheck(List checks, List found, GameObject host, HashSet subScenePaths, string purpose) where T : MonoBehaviour { string typeName = typeof(T).Name; if (found.Count == 0) { checks.Add(new Check { Severity = Severity.Error, Message = $"{typeName} is missing - no {purpose}.", Context = host, FixLabel = host != null ? $"Add to '{host.name}'" : null, Fix = host != null ? () => { Undo.AddComponent(host); EditorSceneManager.MarkSceneDirty(host.scene); } : null }); return; } if (found.Count > 1) { checks.Add(new Check { Severity = Severity.Error, Message = $"{found.Count} {typeName} components in the loaded scenes " + $"({string.Join(", ", found.Select(f => f.gameObject.name))}). " + "These are singletons - every instance after the first destroys itself at Awake, " + "so which one survives is load order dependent. Keep exactly one.", Context = found[0] }); return; } var instance = found[0]; if (subScenePaths.Contains(instance.gameObject.scene.path)) { checks.Add(new Check { Severity = Severity.Error, Message = $"{typeName} on '{instance.gameObject.name}' is inside the subscene " + $"'{instance.gameObject.scene.name}'. MonoBehaviours in a subscene are baked away " + "and never run. Move it to the main scene.", Context = instance }); } } private void CheckPlayer(List doors, HashSet subScenePaths) { var checks = new List(); var triggerables = FindAll(); var playerAuthorings = FindAll(); if (triggerables.Count == 0) { var candidate = playerAuthorings.FirstOrDefault(); checks.Add(new Check { Severity = Severity.Error, Message = "No DoorTriggerableAuthoring in the loaded scenes. Door detection only reacts to " + "entities carrying it, so no door will ever open. It belongs on the PlayerAuthoring " + "object inside your world subscene - that object bakes into the entity that follows " + "Camera.main.", Context = candidate, FixLabel = candidate != null ? $"Add to '{candidate.gameObject.name}'" : null, Fix = candidate != null ? () => { Undo.AddComponent(candidate.gameObject); EditorSceneManager.MarkSceneDirty(candidate.gameObject.scene); } : null }); } foreach (var triggerable in triggerables) { var go = triggerable.gameObject; if (!subScenePaths.Contains(go.scene.path)) { checks.Add(new Check { Severity = Severity.Error, Message = $"DoorTriggerableAuthoring on '{go.name}' is in '{go.scene.name}', which is not " + "a subscene. Authoring components only become entities when a subscene is baked, " + "so this one is inert and doors will not detect the player. Put it on the " + "PlayerAuthoring object inside the world subscene instead.", Context = triggerable }); } else if (go.GetComponent() == null) { checks.Add(new Check { Severity = Severity.Info, Message = $"'{go.name}' is door-triggerable but has no PlayerAuthoring, so its entity stays " + "where it was baked. That is correct for static NPCs or props; the player object " + "needs PlayerAuthoring to follow Camera.main.", Context = triggerable }); } CheckTriggerableLayer(checks, triggerable, doors); } if (playerAuthorings.Count == 0) { checks.Add(new Check { Severity = Severity.Warning, Message = "No PlayerAuthoring found in the open scenes. The entity that represents the player " + "for door detection comes from it. If your world subscene is closed, open it and " + "re-run these checks." }); } AddSection("Player", checks); } private void CheckTriggerableLayer(List checks, DoorTriggerableAuthoring triggerable, List doors) { int layer = triggerable.gameObject.layer; int layerBit = 1 << layer; var blockingConfigs = doors .Select(d => d.doorConfig) .Where(c => c != null && (c.canOpenLayerMask.value & layerBit) == 0) .Distinct() .ToList(); if (blockingConfigs.Count == 0) return; checks.Add(new Check { Severity = Severity.Error, Message = $"'{triggerable.gameObject.name}' is on layer '{LayerMask.LayerToName(layer)}', which is " + $"excluded by Can Open Layer Mask on {blockingConfigs.Count} DoorConfig(s): " + $"{string.Join(", ", blockingConfigs.Select(c => c.name))}. Those doors will ignore it.", Context = blockingConfigs[0], FixLabel = $"Allow layer '{LayerMask.LayerToName(layer)}'", Fix = () => { foreach (var config in blockingConfigs) { Undo.RecordObject(config, "Allow door trigger layer"); config.canOpenLayerMask = config.canOpenLayerMask.value | layerBit; EditorUtility.SetDirty(config); } AssetDatabase.SaveAssets(); } }); } private void CheckDoors(List doors, HashSet subScenePaths) { var checks = new List(); var closedSubScenes = FindAll().Where(s => s.SceneAsset != null && !s.IsLoaded).ToList(); if (closedSubScenes.Count > 0) { checks.Add(new Check { Severity = Severity.Info, Message = $"{closedSubScenes.Count} subscene(s) are closed " + $"({string.Join(", ", closedSubScenes.Select(s => s.SceneName))}). " + "Their doors cannot be inspected, so the checks below - including the duplicate " + "Door Id check - only cover what is open.", Context = closedSubScenes[0], FixLabel = "Open all subscenes", Fix = () => { Unity.Scenes.Editor.SubSceneUtility.EditScene(closedSubScenes.ToArray()); } }); } if (doors.Count == 0) { checks.Add(new Check { Severity = Severity.Warning, Message = "No DoorAuthoring components in the open scenes." }); AddSection("Doors", checks); return; } CheckDuplicateDoorIds(checks, doors); foreach (var door in doors) { CheckSingleDoor(checks, door, subScenePaths); } if (checks.Count == 0) { checks.Add(new Check { Severity = Severity.Ok, Message = $"{doors.Count} door(s) checked - all correctly configured." }); } AddSection("Doors", checks); } private void CheckDuplicateDoorIds(List checks, List doors) { var duplicateGroups = doors .GroupBy(d => d.doorId) .Where(g => g.Count() > 1) .ToList(); foreach (var group in duplicateGroups) { var members = group.ToList(); checks.Add(new Check { Severity = Severity.Error, Message = $"Door Id {group.Key} is used by {members.Count} doors " + $"({string.Join(", ", members.Select(d => d.gameObject.name))}). " + "Door Ids key the collider pool, the audio pool and the lock events, so duplicates " + "make all but one of these doors lose their collider and their sound - and older " + "builds threw \"An item with the same key has already been added\" here.", Context = members[0], FixLabel = "Assign unique Door Ids", Fix = () => AssignUniqueDoorIds(members.Skip(1).ToList(), doors) }); } } private void CheckSingleDoor(List checks, DoorAuthoring door, HashSet subScenePaths) { var go = door.gameObject; string label = $"Door '{go.name}' (id {door.doorId})"; if (!subScenePaths.Contains(go.scene.path)) { checks.Add(new Check { Severity = Severity.Error, Message = $"{label} is in '{go.scene.name}', which is not a subscene. DoorAuthoring only " + "becomes a door entity when a subscene is baked, so this door will not work.", Context = door }); } if (door.doorConfig == null) { checks.Add(new Check { Severity = Severity.Error, Message = $"{label} has no DoorConfig - baking skips it entirely. Assign one, or create a set " + "with the button at the top of this window.", Context = door }); return; // every remaining check reads the config } if (door.doorAudioConfig == null) { checks.Add(new Check { Severity = Severity.Info, Message = $"{label} has no DoorAudioConfiguration and will be silent.", Context = door }); } if (door.triggerVolumeObject == null) { checks.Add(new Check { Severity = Severity.Warning, Message = $"{label} has no Trigger Volume Object. Baking falls back to a 3x3x3 box at the door " + "root, and the AudioSource is parked on the root instead of mid-doorway.", Context = door }); } else if (door.triggerVolumeObject.GetComponent() == null) { checks.Add(new Check { Severity = Severity.Error, Message = $"{label} points at '{door.triggerVolumeObject.name}' as its trigger volume, but that " + "object has no DoorTriggerVolumeAuthoring, so the authored size is ignored.", Context = door.triggerVolumeObject, FixLabel = "Add DoorTriggerVolumeAuthoring", Fix = () => { Undo.AddComponent(door.triggerVolumeObject.gameObject); EditorSceneManager.MarkSceneDirty(go.scene); } }); } bool isDouble = door.doorConfig.doorCount == DoorConfig.DoorCountEnum.Double; if (isDouble) { if (door.leftDoorMesh == null || door.rightDoorMesh == null) { checks.Add(new Check { Severity = Severity.Error, Message = $"{label} uses a Double config but " + (door.leftDoorMesh == null && door.rightDoorMesh == null ? "neither panel is assigned." : $"the {(door.leftDoorMesh == null ? "left" : "right")} panel is not assigned."), Context = door }); } CheckPanelColliders(checks, door); } else { if (door.doorMesh == null) { checks.Add(new Check { Severity = Severity.Error, Message = $"{label} uses a Single config but Door Mesh is not assigned - nothing will animate.", Context = door }); } CheckPanelColliders(checks, door); } } /// /// Shared with the DoorAuthoring inspector's check section (DoorColliderChecks), so the /// validator and the inspector always report the same panel-collider findings. /// private void CheckPanelColliders(List checks, DoorAuthoring door) { var findings = new List(); DoorColliderChecks.Analyze(door, findings); foreach (var finding in findings) { checks.Add(new Check { Severity = finding.Level == DoorColliderChecks.Level.Warning ? Severity.Warning : Severity.Info, Message = $"Door '{door.gameObject.name}' (id {door.doorId}): {finding.Message}", Context = finding.Context != null ? finding.Context : door }); } } /// /// Compares what each subscene AUTHORS (door ids, read live or text-scanned from the scene /// file - no need to open anything) against the door ids that actually EXIST as baked /// entities in the world. A door authored but not baked is exactly the "looks configured /// but never makes a sound or moves" failure: stale bake, still importing, or skipped at /// bake (missing DoorConfig). A baked id no subscene authors is a stale leftover. /// private void CheckSubSceneBakes() { var checks = new List(); SubSceneDoorScanner.ClearCache(); var scanned = SubSceneDoorScanner.ScanAll(); if (scanned.Count == 0) return; SubSceneDoorScanner.BakedDoorIds(out var baked, out var bakedDisabled); int authoredTotal = scanned.Sum(s => s.AuthoringIds.Count); if (baked.Count == 0 && bakedDisabled.Count == 0 && authoredTotal > 0) { checks.Add(new Check { Severity = Severity.Warning, Message = $"{authoredTotal} door(s) authored across {scanned.Count} subscene(s) but no baked " + "door entities exist in the world yet. Subscenes may still be importing - wait for " + "the import to finish and re-run, or open the subscenes to trigger baking." }); AddSection("Subscene bake", checks); return; } // Duplicate ids across ALL subscenes, open or closed - the open-scenes duplicate check // above cannot see into closed ones. var duplicates = scanned .SelectMany(s => s.AuthoringIds.Select(id => (id, s))) .GroupBy(t => t.id) .Where(g => g.Count() > 1) .ToList(); foreach (var group in duplicates) { checks.Add(new Check { Severity = Severity.Error, Message = $"Door id {group.Key} is authored {group.Count()} times across subscene(s) " + $"{string.Join(", ", group.Select(t => t.s.SubScene.SceneName).Distinct())}. " + "Ids key the pools and audio registrations, so all but one of these doors go silent.", Context = group.First().s.SubScene }); } foreach (var entry in scanned) { if (entry.AuthoringIds.Count == 0) continue; var subScene = entry.SubScene; var notEnabled = entry.AuthoringIds.Where(id => !baked.Contains(id)).Distinct().ToList(); if (notEnabled.Count == 0) continue; // Baked, but from an inactive GameObject: the entity exists with a Disabled tag, // so the bake is fine - the door is just switched off. Deliberate for door // variants; a mistake when the door was supposed to work. var disabledOnes = notEnabled.Where(id => bakedDisabled.Contains(id)).ToList(); if (disabledOnes.Count > 0) { bool knownInactive = disabledOnes.All(id => entry.InactiveAuthoringIds.Contains(id)); checks.Add(new Check { Severity = knownInactive ? Severity.Info : Severity.Warning, Message = $"Subscene '{subScene.SceneName}': door id(s) {string.Join(", ", disabledOnes)} " + "are baked but DISABLED (their GameObject is inactive). A disabled door does " + "not detect, move, or make a sound until something activates it at runtime. " + "Fine for intentionally switched-off door variants - if one of these should " + "work, activate its GameObject and save the subscene.", Context = subScene }); } var missing = notEnabled.Except(disabledOnes).ToList(); if (missing.Count == 0) continue; checks.Add(new Check { Severity = Severity.Error, Message = $"Subscene '{subScene.SceneName}': door id(s) {string.Join(", ", missing)} are " + "authored but have NO baked entity - those doors will not move or make a sound. " + "Stale bake, import still running, or the door was skipped at bake (missing " + "DoorConfig). Opening the subscene re-bakes it live.", Context = subScene, FixLabel = entry.IsLoaded ? null : $"Open '{subScene.SceneName}'", Fix = entry.IsLoaded ? null : () => Unity.Scenes.Editor.SubSceneUtility.EditScene(subScene) }); } var authoredEverywhere = new HashSet(scanned.SelectMany(s => s.AuthoringIds)); var stale = baked.Concat(bakedDisabled) .Where(id => !authoredEverywhere.Contains(id)).Distinct().OrderBy(id => id).ToList(); if (stale.Count > 0) { checks.Add(new Check { Severity = Severity.Warning, Message = $"Baked door entity id(s) {string.Join(", ", stale)} exist in the world but no " + "subscene in the hierarchy authors them - stale bakes (an id was changed after " + "baking) or doors from a scene outside this hierarchy. If a door recently got a " + "new id, its old baked entity may still be answering events meant for nobody." }); } if (checks.Count == 0) { checks.Add(new Check { Severity = Severity.Ok, Message = $"{authoredTotal} authored door(s) across {scanned.Count} subscene(s) - every " + "authored id has a baked entity, and no stale baked ids found." }); } AddSection("Subscene bake", checks); } /// /// Sweeps every DoorAudioConfiguration asset in the project (not just those on open doors) /// through DoorAudioConfigurationValidator - the same rules DoorAudioBridge lives by at /// runtime - so a half-authored or broken config is caught here instead of as a door that /// plays its open sound but not its close sound in playmode. /// private void CheckAudioConfigurations(List doors) { var checks = new List(); int total = 0; foreach (var guid in AssetDatabase.FindAssets("t:DoorAudioConfiguration")) { var path = AssetDatabase.GUIDToAssetPath(guid); var config = AssetDatabase.LoadAssetAtPath(path); if (config == null) continue; total++; var issues = DoorAudioConfigurationValidator.Validate(config); if (issues.Count == 0) continue; int usedBy = doors.Count(d => d.doorAudioConfig == config); string usage = usedBy > 0 ? $" (used by {usedBy} open door(s))" : ""; foreach (var issue in issues) { checks.Add(new Check { Severity = issue.IsError ? Severity.Error : Severity.Warning, Message = $"'{config.name}'{usage}: {issue.Message}", Context = issue.Context != null ? issue.Context : config }); } } if (checks.Count == 0 && total > 0) { checks.Add(new Check { Severity = Severity.Ok, Message = $"{total} DoorAudioConfiguration asset(s) checked - all correctly authored." }); } AddSection("Audio configurations", checks); } private void CheckLegacyIdentifiers(List doors) { var identifiers = FindAll(); if (identifiers.Count == 0) return; int migratable = identifiers.Count(i => i.audioConfiguration != null && doors.Any(d => d.doorId == i.doorNumber && d.doorAudioConfig == null)); AddSection("Legacy DoorIdentifier objects", new List { new() { Severity = Severity.Warning, Message = $"{identifiers.Count} DoorIdentifier object(s) are still in the loaded scenes. " + "They no longer do anything - audio configuration now lives on DoorAuthoring and is " + "baked into the door entity. " + (migratable > 0 ? $"{migratable} of them hold a configuration that the matching door is missing." : "Their configurations are already on the matching doors, or no matching door is open."), Context = identifiers[0], FixLabel = "Migrate and delete", Fix = () => MigrateLegacyIdentifiers(identifiers, doors) } }); } #endregion #region Fixes private void CreateDoorManagement() { var go = new GameObject("DoorManagement"); Undo.RegisterCreatedObjectUndo(go, "Create DoorManagement"); go.AddComponent(); go.AddComponent(); go.AddComponent(); go.AddComponent(); EditorSceneManager.MarkSceneDirty(go.scene); Selection.activeGameObject = go; } private static void AssignUniqueDoorIds(List toRenumber, List allDoors) { var usedIds = new HashSet(allDoors.Select(d => d.doorId)); int nextId = usedIds.Count == 0 ? 1 : usedIds.Max() + 1; foreach (var door in toRenumber) { while (usedIds.Contains(nextId)) nextId++; Undo.RecordObject(door, "Assign unique Door Id"); door.doorId = nextId; usedIds.Add(nextId); EditorUtility.SetDirty(door); EditorSceneManager.MarkSceneDirty(door.gameObject.scene); } } private static void MigrateLegacyIdentifiers(List identifiers, List doors) { int migrated = 0; int skipped = 0; foreach (var identifier in identifiers) { var door = doors.FirstOrDefault(d => d.doorId == identifier.doorNumber); if (identifier.audioConfiguration != null && door != null && door.doorAudioConfig == null) { Undo.RecordObject(door, "Migrate door audio configuration"); door.doorAudioConfig = identifier.audioConfiguration; EditorUtility.SetDirty(door); EditorSceneManager.MarkSceneDirty(door.gameObject.scene); migrated++; } else if (identifier.audioConfiguration != null && door == null) { // The matching door lives in a closed subscene - deleting now would lose the reference. skipped++; continue; } var go = identifier.gameObject; bool isBareHolder = go.transform.childCount == 0 && go.GetComponents().Length == 2; if (isBareHolder) { Undo.DestroyObjectImmediate(go); } else { Undo.DestroyObjectImmediate(identifier); } } string message = $"Moved {migrated} audio configuration(s) onto the matching DoorAuthoring."; if (skipped > 0) { message += $"\n\n{skipped} DoorIdentifier(s) were left alone because no open door matches their " + "Door Number. Open the subscene that holds those doors and run this again."; } EditorUtility.DisplayDialog("Migrate DoorIdentifier", message, "OK"); } #endregion #region Asset creation private static void CreateDoorConfigSet() { string folder = PromptForProjectFolder("Where should the DoorConfig assets go?"); if (folder == null) return; // Telescopic is a style of Sliding Double rather than its own movement type, so the set // carries both variants - otherwise the only way to get one is to hand-edit a config. var variants = new (string name, DoorConfig.DoorCountEnum count, DoorConfig.DoorMovementEnum movement, DoorConfig.SlidingStyleEnum slidingStyle)[] { ("DoorConfig_SingleRotating", DoorConfig.DoorCountEnum.Single, DoorConfig.DoorMovementEnum.Rotating, DoorConfig.SlidingStyleEnum.Mirrored), ("DoorConfig_DoubleRotating", DoorConfig.DoorCountEnum.Double, DoorConfig.DoorMovementEnum.Rotating, DoorConfig.SlidingStyleEnum.Mirrored), ("DoorConfig_SingleSliding", DoorConfig.DoorCountEnum.Single, DoorConfig.DoorMovementEnum.Sliding, DoorConfig.SlidingStyleEnum.Mirrored), ("DoorConfig_DoubleSliding", DoorConfig.DoorCountEnum.Double, DoorConfig.DoorMovementEnum.Sliding, DoorConfig.SlidingStyleEnum.Mirrored), ("DoorConfig_DoubleSlidingTelescopic", DoorConfig.DoorCountEnum.Double, DoorConfig.DoorMovementEnum.Sliding, DoorConfig.SlidingStyleEnum.Telescopic), }; var created = new List(); foreach (var (name, count, movement, slidingStyle) in variants) { string path = AssetDatabase.GenerateUniqueAssetPath($"{folder}/{name}.asset"); var config = CreateInstance(); config.doorCount = count; config.doorMovement = movement; config.slidingStyle = slidingStyle; AssetDatabase.CreateAsset(config, path); created.Add(System.IO.Path.GetFileName(path)); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); EditorUtility.DisplayDialog("DoorConfig assets created", $"Created in {folder}:\n\n{string.Join("\n", created)}\n\n" + "Adjust angles, slide offset, timings and Can Open Layer Mask on each, then assign them to your doors.", "OK"); Selection.activeObject = AssetDatabase.LoadAssetAtPath($"{folder}/{created[0]}"); } private static void CreateDoorAudioConfiguration() { string path = EditorUtility.SaveFilePanelInProject( "Create DoorAudioConfiguration", "DoorAudioConfig", "asset", "Where should the door audio configuration be saved?"); if (string.IsNullOrEmpty(path)) return; var config = CreateInstance(); AssetDatabase.CreateAsset(config, path); AssetDatabase.SaveAssets(); Selection.activeObject = config; EditorGUIUtility.PingObject(config); } private static void CreateAudioSourcePrefab(AudioSourcePoolManager assignTo = null) { string path = EditorUtility.SaveFilePanelInProject( "Create pooled AudioSource prefab", "PooledDoorAudioSource", "prefab", "Where should the pooled AudioSource prefab be saved?"); if (string.IsNullOrEmpty(path)) return; var temp = new GameObject("PooledDoorAudioSource"); try { var audioSource = temp.AddComponent(); audioSource.playOnAwake = false; audioSource.spatialBlend = 1f; audioSource.minDistance = 1f; audioSource.maxDistance = 25f; audioSource.rolloffMode = AudioRolloffMode.Linear; // Per-source Steam Audio settings are overwritten from the DoorAudioConfiguration at runtime; // the component only has to be present for that to have somewhere to land. temp.AddComponent(); var prefab = PrefabUtility.SaveAsPrefabAsset(temp, path); if (assignTo != null && prefab != null) { Undo.RecordObject(assignTo, "Assign pooled AudioSource prefab"); assignTo.audioSourcePrefab = prefab; EditorUtility.SetDirty(assignTo); EditorSceneManager.MarkSceneDirty(assignTo.gameObject.scene); } Selection.activeObject = prefab; EditorGUIUtility.PingObject(prefab); } finally { DestroyImmediate(temp); } } private static string PromptForProjectFolder(string title) { string absolute = EditorUtility.SaveFolderPanel(title, "Assets", ""); if (string.IsNullOrEmpty(absolute)) return null; absolute = absolute.Replace('\\', '/'); string dataPath = Application.dataPath.Replace('\\', '/'); if (absolute == dataPath) return "Assets"; if (absolute.StartsWith(dataPath + "/")) return "Assets" + absolute.Substring(dataPath.Length); EditorUtility.DisplayDialog("Folder outside the project", "Assets have to live under the project's Assets folder. Pick a folder inside it.", "OK"); return null; } #endregion #region Helpers private void AddSection(string title, List checks) { if (checks.Count == 0) return; // Worst first, so a long list of per-door notes never buries a blocking error. checks.Sort((a, b) => b.Severity.CompareTo(a.Severity)); _sections.Add((title, checks)); } private static List FindAll() where T : Object { return Object.FindObjectsByType(FindObjectsInactive.Include).ToList(); } /// Paths of the scenes backing every currently open SubScene. private static HashSet GetOpenSubScenePaths() { var paths = new HashSet(); foreach (var subScene in FindAll()) { if (subScene.SceneAsset == null) continue; string path = AssetDatabase.GetAssetPath(subScene.SceneAsset); if (!string.IsNullOrEmpty(path)) { paths.Add(path); } } return paths; } #endregion } } #endif