219 lines
6.9 KiB
C#
219 lines
6.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using NightclubArcadia.Skills;
|
|
using UnityEditor;
|
|
using UnityEditor.Callbacks;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates/updates ScriptableObject assets without opening a scene.
|
|
/// Safe to run on domain reload.
|
|
/// </summary>
|
|
public static void EnsureCandidateAssetsOnly()
|
|
{
|
|
if (!TryLoadRoster(out var roster))
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureFolder("Assets", "Candidates");
|
|
EnsureFolder("Assets/Candidates", "Definitions");
|
|
|
|
var definitions = new List<CandidateDefinition>();
|
|
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var entry in roster)
|
|
{
|
|
rosterIds.Add(entry.id);
|
|
definitions.Add(CreateOrUpdateDefinition(entry));
|
|
}
|
|
|
|
DeleteOrphanDefinitions(rosterIds);
|
|
|
|
var database = AssetDatabase.LoadAssetAtPath<CandidateDatabase>(DatabasePath);
|
|
if (database == null)
|
|
{
|
|
database = ScriptableObject.CreateInstance<CandidateDatabase>();
|
|
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<TextAsset>(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<CandidateRoot>(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<SkillAxis>(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<CandidateEvidenceType>(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<CandidateClueStatus>(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<CandidateDefinition>(path);
|
|
if (asset == null)
|
|
{
|
|
asset = ScriptableObject.CreateInstance<CandidateDefinition>();
|
|
AssetDatabase.CreateAsset(asset, path);
|
|
}
|
|
|
|
asset.EditorSet(entry);
|
|
EditorUtility.SetDirty(asset);
|
|
return asset;
|
|
}
|
|
|
|
static void DeleteOrphanDefinitions(HashSet<string> 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);
|
|
}
|
|
}
|
|
}
|