using System; using System.Collections.Generic; using System.IO; using NightclubArcadia.Skills; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; /// /// Regenerates CandidateDefinition assets + CandidateDatabase from candidates.json. /// /// The JSON is the source of truth; .asset files are build output and hand-edits are lost /// on script reload by EnsureCandidateAssetsOnly. /// public static class CandidateSystemSetup { const string CandidatesFolder = "Assets/Candidates"; const string DefinitionsFolder = "Assets/Candidates/Definitions"; const string JsonPath = "Assets/Candidates/candidates.json"; const string DatabasePath = "Assets/Candidates/CandidateDatabase.asset"; const string AutoSetupSessionKey = "NightclubArcadia.CandidateSystem.AutoSetupDone"; [DidReloadScripts] static void AutoSetupAfterReload() { if (EditorApplication.isPlayingOrWillChangePlaymode) { return; } if (SessionState.GetBool(AutoSetupSessionKey, false)) { return; } EditorApplication.delayCall += () => { if (SessionState.GetBool(AutoSetupSessionKey, false)) { return; } SessionState.SetBool(AutoSetupSessionKey, true); EnsureCandidateAssetsOnly(); }; } [MenuItem("Tools/Nightclub Arcadia/Set Up Candidate System")] public static void SetUpCandidateSystem() { EnsureCandidateAssetsOnly(); } /// /// Creates/updates ScriptableObject assets without opening a scene. /// Safe to run on domain reload. /// public static void EnsureCandidateAssetsOnly() { if (!TryLoadRoster(out var roster)) { return; } EnsureFolder("Assets", "Candidates"); EnsureFolder("Assets/Candidates", "Definitions"); var definitions = new List(); var rosterIds = new HashSet(StringComparer.Ordinal); foreach (var entry in roster) { rosterIds.Add(entry.id); definitions.Add(CreateOrUpdateDefinition(entry)); } DeleteOrphanDefinitions(rosterIds); var database = AssetDatabase.LoadAssetAtPath(DatabasePath); if (database == null) { database = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(database, DatabasePath); } database.EditorSetCandidates(definitions); EditorUtility.SetDirty(database); AssetDatabase.SaveAssets(); Debug.Log($"[CandidateSystemSetup] Candidate assets ready at {DatabasePath}."); } static bool TryLoadRoster(out CandidateEntry[] roster) { roster = null; var text = AssetDatabase.LoadAssetAtPath(JsonPath); if (text == null) { // JSON may exist on disk before Unity has imported it as a TextAsset. if (!File.Exists(JsonPath)) { Debug.LogError($"[CandidateSystemSetup] Missing registry at {JsonPath}."); return false; } text = new TextAsset(File.ReadAllText(JsonPath)); } CandidateRoot root; try { root = JsonUtility.FromJson(text.text); } catch (Exception ex) { Debug.LogError($"[CandidateSystemSetup] Failed to parse {JsonPath}: {ex.Message}"); return false; } if (root?.candidates == null || root.candidates.Length == 0) { Debug.LogError($"[CandidateSystemSetup] {JsonPath} has no candidates."); return false; } foreach (var entry in root.candidates) { if (entry == null || string.IsNullOrEmpty(entry.id)) { Debug.LogError($"[CandidateSystemSetup] {JsonPath} contains a candidate with empty id."); return false; } if (!Enum.TryParse(entry.intuitionAxis, ignoreCase: true, out _)) { Debug.LogError( $"[CandidateSystemSetup] Candidate '{entry.id}': invalid intuitionAxis '{entry.intuitionAxis}'."); return false; } if (entry.clues == null) { continue; } foreach (var clue in entry.clues) { if (clue == null || string.IsNullOrEmpty(clue.id)) { Debug.LogError($"[CandidateSystemSetup] Candidate '{entry.id}' has a clue with empty id."); return false; } if (!Enum.TryParse(clue.type, ignoreCase: true, out _)) { Debug.LogError( $"[CandidateSystemSetup] Clue '{clue.id}': invalid type '{clue.type}'."); return false; } var status = string.IsNullOrEmpty(clue.status) ? "Planned" : clue.status; if (!Enum.TryParse(status, ignoreCase: true, out _)) { Debug.LogError( $"[CandidateSystemSetup] Clue '{clue.id}': invalid status '{clue.status}'."); return false; } } } roster = root.candidates; return true; } static CandidateDefinition CreateOrUpdateDefinition(CandidateEntry entry) { var path = $"{DefinitionsFolder}/{entry.id}.asset"; var asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) { asset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(asset, path); } asset.EditorSet(entry); EditorUtility.SetDirty(asset); return asset; } static void DeleteOrphanDefinitions(HashSet rosterIds) { if (!AssetDatabase.IsValidFolder(DefinitionsFolder)) { return; } var guids = AssetDatabase.FindAssets("t:CandidateDefinition", new[] { DefinitionsFolder }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var fileName = Path.GetFileNameWithoutExtension(path); if (rosterIds.Contains(fileName)) { continue; } Debug.Log($"[CandidateSystemSetup] Deleting orphan definition {path}"); AssetDatabase.DeleteAsset(path); } } static void EnsureFolder(string parent, string child) { var path = $"{parent}/{child}"; if (!AssetDatabase.IsValidFolder(path)) { AssetDatabase.CreateFolder(parent, child); } } }