using System.Collections.Generic; using jeanf.audiosystems; using UnityEngine; namespace AutomaticDoorSystem { /// /// Edit-time checks for DoorAudioConfiguration assets, mirroring exactly what DoorAudioBridge /// does at runtime: the SamplerData list is tried first and the legacy clip list is the /// fallback, per event, with a random pick inside whichever list answers. Anything flagged /// here is therefore a sound that will be missing, silent or inconsistent in playmode. /// Shared by the config's inspector, the Setup Validator window and the editor tests. /// public static class DoorAudioConfigurationValidator { public readonly struct Issue { public readonly bool IsError; public readonly string Message; /// The sub-object the issue is about (a referenced SamplerData), when there is one. public readonly Object Context; public Issue(bool isError, string message, Object context = null) { IsError = isError; Message = message; Context = context; } } public static List Validate(DoorAudioConfiguration config) { var issues = new List(); if (config == null) { issues.Add(new Issue(true, "DoorAudioConfiguration reference is null.")); return issues; } if (config.volume <= 0f) { issues.Add(new Issue(false, "volume is 0 - every door sound from this config plays inaudibly.")); } if (config.maxDistance <= config.minDistance) { issues.Add(new Issue(false, $"maxDistance ({config.maxDistance}) is not greater than minDistance ({config.minDistance}) - " + "3D rolloff is degenerate and audibility becomes unpredictable.")); } bool openPlays = CheckEvent(issues, "open", config.openSamplerData, config.openSoundClips); bool closePlays = CheckEvent(issues, "close", config.closeSamplerData, config.closeSoundClips); CheckEvent(issues, "lock", config.lockSamplerData, config.lockSoundClips); CheckEvent(issues, "unlock", config.unlockSamplerData, config.unlockSoundClips); // The classic half-authored config: the door announces itself on open and then goes // quiet, which reads as a bug in playmode rather than as a missing asset. if (openPlays && !closePlays) { issues.Add(new Issue(false, "an open sound is authored but no playable close sound is - the door will sound " + "when opening and stay silent when closing.")); } else if (closePlays && !openPlays) { issues.Add(new Issue(false, "a close sound is authored but no playable open sound is - the door will sound " + "when closing and stay silent when opening.")); } if (!openPlays && !closePlays) { issues.Add(new Issue(false, "neither open nor close has a playable sound - a door using this config is silent. " + "Leave doorAudioConfig unassigned instead if that is intended.")); } return issues; } /// /// Validates one event's two lists and reports whether the event can actually produce /// sound at runtime (some sampler entry with a clip, or some legacy clip). /// private static bool CheckEvent(List issues, string eventName, SamplerData[] samplerData, AudioClip[] clips) { bool samplerCanPlay = false; bool samplerHasEntries = samplerData != null && samplerData.Length > 0; bool anyEntryFallsThrough = false; if (samplerHasEntries) { for (int i = 0; i < samplerData.Length; i++) { var data = samplerData[i]; if (data == null) { // The bridge picks one entry at random; a null pick falls through to the // legacy clips, so the event randomly alternates between two sounds (or silence). issues.Add(new Issue(true, $"{eventName} SamplerData slot {i} is empty - when the random pick lands on it " + "the event falls back to the legacy clips (or plays nothing).")); anyEntryFallsThrough = true; continue; } var dataIssues = SamplerDataValidation.Validate(data); foreach (var dataIssue in dataIssues) { issues.Add(new Issue(dataIssue.IsError, $"{eventName} SamplerData '{data.name}': {dataIssue.Message}", data)); } if (data.audioClip == null) { anyEntryFallsThrough = true; } else if (!dataIssues.Exists(d => d.IsError)) { samplerCanPlay = true; } } } bool clipsCanPlay = false; if (clips != null && clips.Length > 0) { for (int i = 0; i < clips.Length; i++) { if (clips[i] == null) { issues.Add(new Issue(true, $"{eventName} sound clip slot {i} is empty - when the random pick lands on it " + "the event plays nothing.")); } else { clipsCanPlay = true; } } } // "Can play" means some runtime outcome produces sound: a healthy sampler entry, the // clip list when no sampler entries are authored, or the clip list reached through a // sampler entry that falls through (null entry / entry without a clip). return samplerCanPlay || (!samplerHasEntries && clipsCanPlay) || (anyEntryFallsThrough && clipsCanPlay); } } }