using System; using System.Collections.Generic; namespace IdleSpectator; /// Live asset/task lookup and species-token hygiene. No voice classification. public static class ActivityAssetCatalog { private static HashSet _liveSpeciesPlaceTokens; private static int _liveSpeciesPlaceTokensCount = -1; public static ActorAsset TryGetActorAsset(string speciesId) { if (string.IsNullOrEmpty(speciesId)) { return null; } try { return AssetManager.actor_library?.get(speciesId.Trim()); } catch { return null; } } public static bool IsLiveActorAssetId(string speciesId) { if (string.IsNullOrEmpty(speciesId)) { return false; } try { return AssetManager.actor_library != null && AssetManager.actor_library.has(speciesId.Trim()); } catch { return false; } } public static string SpeciesDisplayLabel(ActorAsset asset) { if (asset == null) { return ""; } try { string translated = asset.getTranslatedName(); if (!string.IsNullOrEmpty(translated)) { string localeId = asset.getLocaleID() ?? ""; if (!translated.Equals(localeId, StringComparison.Ordinal) && translated.IndexOf('_') < 0) { return translated.Trim().ToLowerInvariant(); } } } catch { // Localization may not be ready during early harness setup. } if (!string.IsNullOrEmpty(asset.name_locale)) { return asset.name_locale.Trim().ToLowerInvariant(); } return string.IsNullOrEmpty(asset.id) ? "" : asset.id.Replace('_', ' ').Trim(); } public static string SpeciesDisplayLabel(string speciesId) { ActorAsset asset = TryGetActorAsset(speciesId); return asset != null ? SpeciesDisplayLabel(asset) : (string.IsNullOrEmpty(speciesId) ? "" : speciesId.Replace('_', ' ').Trim()); } /// Title Case species for dossier nametag: Unicorn, not unicorn. public static string SpeciesNametagLabel(string speciesId) { string raw = SpeciesDisplayLabel(speciesId); return TitleCaseWords(string.IsNullOrEmpty(raw) ? speciesId : raw); } /// /// Live prefixes like gatherer_* where ≥2 jobs share P_* and bare P /// is not itself a job. Rebuilt from . /// private static HashSet _resourceRoleJobPrefixes; private static int _resourceRoleJobPrefixesCount = -1; public static CitizenJobAsset TryGetCitizenJobAsset(string jobId) { if (string.IsNullOrEmpty(jobId)) { return null; } try { return AssetManager.citizen_job_library?.get(jobId.Trim()); } catch { return null; } } public static List EnumerateLiveCitizenJobIds() { var ids = new List(); try { var lib = AssetManager.citizen_job_library; if (lib?.list == null) { return ids; } for (int i = 0; i < lib.list.Count; i++) { CitizenJobAsset asset = lib.list[i]; if (asset != null && !string.IsNullOrEmpty(asset.id)) { ids.Add(asset.id); } } } catch { // Library can be unavailable during early boot. } return ids; } /// /// Live citizen-job display for dossier from citizen_job_library ids. /// Resource-role families discovered from the library /// (gatherer_herbsHerb Gatherer); everything else is Title Case id /// (miner_depositMiner Deposit). Never probes invented locale keys. /// public static string JobDisplayLabel(string jobId) { if (string.IsNullOrEmpty(jobId)) { return ""; } string id = jobId.Trim(); CitizenJobAsset asset = TryGetCitizenJobAsset(id); if (asset != null && !string.IsNullOrEmpty(asset.id)) { id = asset.id; } return FormatCitizenJobIdLabel(id); } public static string JobDisplayLabel(CitizenJobAsset asset) { if (asset == null || string.IsNullOrEmpty(asset.id)) { return ""; } return FormatCitizenJobIdLabel(asset.id); } /// /// Stable identity role for the dossier nametag. Simulation variants such as /// miner_deposit describe the current assignment, not a distinct profession. /// Activity/task prose may still use the full job label. /// public static string IdentityJobDisplayLabel(string jobId) { if (string.IsNullOrEmpty(jobId)) { return ""; } string id = jobId.Trim(); const string depositSuffix = "_deposit"; if (id.EndsWith(depositSuffix, StringComparison.OrdinalIgnoreCase) && id.Length > depositSuffix.Length) { id = id.Substring(0, id.Length - depositSuffix.Length); } return JobDisplayLabel(id); } public static string IdentityJobDisplayLabel(CitizenJobAsset asset) => asset == null ? "" : IdentityJobDisplayLabel(asset.id); /// /// Title Case + discovered resource-role reorder for a citizen job id. /// Public for harness audits over the full live library. /// public static string FormatCitizenJobIdLabel(string jobId) { if (string.IsNullOrEmpty(jobId)) { return ""; } string id = jobId.Trim(); if (id.StartsWith("job_", StringComparison.OrdinalIgnoreCase)) { id = id.Substring(4); } EnsureResourceRoleJobPrefixes(); int us = id.IndexOf('_'); if (us > 0 && us < id.Length - 1 && _resourceRoleJobPrefixes != null && _resourceRoleJobPrefixes.Contains(id.Substring(0, us))) { string resource = SingularizeResourcePhrase(id.Substring(us + 1).Replace('_', ' ')); if (!string.IsNullOrEmpty(resource)) { return TitleCaseWords(resource + " " + id.Substring(0, us)); } } return TitleCaseWords(id.Replace('_', ' ')); } private static void EnsureResourceRoleJobPrefixes() { List ids = EnumerateLiveCitizenJobIds(); if (_resourceRoleJobPrefixes != null && _resourceRoleJobPrefixesCount == ids.Count) { return; } var idSet = new HashSet(StringComparer.OrdinalIgnoreCase); var prefixCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < ids.Count; i++) { string id = ids[i]; if (string.IsNullOrEmpty(id)) { continue; } idSet.Add(id); int us = id.IndexOf('_'); if (us <= 0 || us >= id.Length - 1) { continue; } string prefix = id.Substring(0, us); if (prefixCounts.TryGetValue(prefix, out int count)) { prefixCounts[prefix] = count + 1; } else { prefixCounts[prefix] = 1; } } var families = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair kv in prefixCounts) { // Family: several P_* jobs and no bare job id P (avoids miner + miner_deposit → Deposit Miner). if (kv.Value >= 2 && !idSet.Contains(kv.Key)) { families.Add(kv.Key); } } _resourceRoleJobPrefixes = families; _resourceRoleJobPrefixesCount = ids.Count; } private static string SingularizeResourcePhrase(string phrase) { if (string.IsNullOrEmpty(phrase)) { return ""; } string s = phrase.Trim(); int lastSpace = s.LastIndexOf(' '); if (lastSpace < 0) { return SingularizeResourceWord(s); } string head = s.Substring(0, lastSpace).Trim(); string last = SingularizeResourceWord(s.Substring(lastSpace + 1).Trim()); return string.IsNullOrEmpty(last) ? head : head + " " + last; } private static string SingularizeResourceWord(string word) { if (string.IsNullOrEmpty(word)) { return ""; } string w = word.Trim(); if (w.EndsWith("hes", StringComparison.OrdinalIgnoreCase) && w.Length > 3) { // bushes → bush return w.Substring(0, w.Length - 2); } if (w.EndsWith("s", StringComparison.OrdinalIgnoreCase) && !w.EndsWith("ss", StringComparison.OrdinalIgnoreCase) && w.Length > 2) { // herbs → herb; honey stays honey return w.Substring(0, w.Length - 1); } return w; } /// Title-case whitespace-separated words for nametag identity tags. public static string TitleCaseWords(string raw) { if (string.IsNullOrEmpty(raw)) { return ""; } string s = raw.Trim().Replace('_', ' '); while (s.IndexOf(" ", StringComparison.Ordinal) >= 0) { s = s.Replace(" ", " "); } if (string.IsNullOrEmpty(s)) { return ""; } char[] chars = s.ToCharArray(); bool newWord = true; for (int i = 0; i < chars.Length; i++) { if (char.IsWhiteSpace(chars[i]) || chars[i] == '-' || chars[i] == '/') { newWord = true; continue; } if (newWord) { chars[i] = char.ToUpperInvariant(chars[i]); newWord = false; } else { chars[i] = char.ToLowerInvariant(chars[i]); } } return new string(chars); } public static List EnumerateLiveActorAssetIds() { var ids = new List(); try { ActorAssetLibrary lib = AssetManager.actor_library; if (lib?.list == null) { return ids; } for (int i = 0; i < lib.list.Count; i++) { ActorAsset asset = lib.list[i]; if (asset != null && !string.IsNullOrEmpty(asset.id)) { ids.Add(asset.id); } } } catch { // Library can be unavailable during early boot. } return ids; } public static List EnumerateLiveStatusIds() { var ids = new List(); try { StatusLibrary lib = AssetManager.status; if (lib?.list == null) { return ids; } for (int i = 0; i < lib.list.Count; i++) { StatusAsset asset = lib.list[i]; if (asset != null && !string.IsNullOrEmpty(asset.id)) { ids.Add(asset.id); } } } catch { // Library can be unavailable during early boot. } return ids; } public static StatusAsset TryGetStatusAsset(string statusId) { if (string.IsNullOrEmpty(statusId)) { return null; } try { return AssetManager.status?.get(statusId.Trim()); } catch { return null; } } public static List EnumerateLiveHappinessIds() { var ids = new List(); try { HappinessLibrary lib = AssetManager.happiness_library; if (lib?.list == null) { return ids; } for (int i = 0; i < lib.list.Count; i++) { HappinessAsset asset = lib.list[i]; if (asset != null && !string.IsNullOrEmpty(asset.id)) { ids.Add(asset.id); } } } catch { // Library can be unavailable during early boot. } return ids; } public static HappinessAsset TryGetHappinessAsset(string effectId) { if (string.IsNullOrEmpty(effectId)) { return null; } try { return AssetManager.happiness_library?.get(effectId.Trim()); } catch { return null; } } public static List EnumerateLiveTaskIds() { return EnumerateLibraryIds("tasks_actor"); } public static List EnumerateLiveWorldLogIds() { return EnumerateLibraryIds("world_log_library"); } public static WorldLogAsset TryGetWorldLogAsset(string assetId) { if (string.IsNullOrEmpty(assetId)) { return null; } try { return AssetManager.world_log_library?.get(assetId.Trim()); } catch { return null; } } public static List EnumerateLiveDisasterIds() { return EnumerateLibraryIds("disasters"); } public static DisasterAsset TryGetDisasterAsset(string disasterId) { if (string.IsNullOrEmpty(disasterId)) { return null; } try { return AssetManager.disasters?.get(disasterId.Trim()); } catch { return null; } } public static List EnumerateLiveWarTypeIds() { return EnumerateLibraryIds("war_types_library"); } public static WarTypeAsset TryGetWarTypeAsset(string warTypeId) { if (string.IsNullOrEmpty(warTypeId)) { return null; } try { return AssetManager.war_types_library?.get(warTypeId.Trim()); } catch { return null; } } public static List EnumerateLivePlotIds() { return EnumerateLibraryIds("plots_library"); } public static List EnumerateLiveEraIds() { return EnumerateLibraryIds("era_library"); } public static List EnumerateLiveBookTypeIds() { return EnumerateLibraryIds("book_types"); } public static List EnumerateLiveTraitIds() { return EnumerateLibraryIds("traits"); } public static List EnumerateLiveBuildingIds() { return EnumerateLibraryIds("buildings"); } public static List EnumerateLiveSpellIds() => EnumerateLibraryIds("spells"); public static List EnumerateLivePowerIds() => EnumerateLibraryIds("powers"); public static List EnumerateLiveDecisionIds() => EnumerateLibraryIds("decisions_library"); public static List EnumerateLiveItemIds() => EnumerateLibraryIds("items"); public static List EnumerateLiveSubspeciesTraitIds() => EnumerateLibraryIds("subspecies_traits"); public static List EnumerateLiveGeneIds() => EnumerateLibraryIds("gene_library"); public static List EnumerateLivePhenotypeIds() => EnumerateLibraryIds("phenotype_library"); public static List EnumerateLiveWorldLawIds() => EnumerateLibraryIds("world_laws_library"); public static List EnumerateLiveBiomeIds() => EnumerateLibraryIds("biome_library"); public static List EnumerateLiveCultureTraitIds() => EnumerateLibraryIds("culture_traits"); public static List EnumerateLiveReligionTraitIds() => EnumerateLibraryIds("religion_traits"); public static List EnumerateLiveClanTraitIds() => EnumerateLibraryIds("clan_traits"); public static List EnumerateLiveLanguageTraitIds() => EnumerateLibraryIds("language_traits"); private static List EnumerateLibraryIds(string assetManagerField) { var ids = new List(); try { object lib = typeof(AssetManager).GetField(assetManagerField)?.GetValue(null); object listObj = lib?.GetType().GetField("list")?.GetValue(lib) ?? lib?.GetType().GetProperty("list")?.GetValue(lib, null); System.Collections.IList list = listObj as System.Collections.IList; if (list == null) { return ids; } for (int i = 0; i < list.Count; i++) { object asset = list[i]; object idObj = asset?.GetType().GetField("id")?.GetValue(asset) ?? asset?.GetType().GetProperty("id")?.GetValue(asset, null); string id = idObj as string; if (!string.IsNullOrEmpty(id)) { ids.Add(id); } } } catch { // Library can be unavailable during early boot. } return ids; } public static bool IsLiveSpeciesPlaceToken(string label) { if (string.IsNullOrEmpty(label)) { return false; } EnsureLiveSpeciesPlaceTokens(); return _liveSpeciesPlaceTokens != null && _liveSpeciesPlaceTokens.Contains(label.Trim()); } private static void EnsureLiveSpeciesPlaceTokens() { try { ActorAssetLibrary lib = AssetManager.actor_library; if (lib?.list == null) { return; } int count = lib.list.Count; if (_liveSpeciesPlaceTokens != null && _liveSpeciesPlaceTokensCount == count) { return; } var tokens = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < count; i++) { ActorAsset asset = lib.list[i]; if (asset == null) { continue; } Add(tokens, asset.id); Add(tokens, asset.kingdom_id_wild); Add(tokens, asset.name_locale); if (!string.IsNullOrEmpty(asset.id)) { Add(tokens, asset.id.Replace('_', ' ')); } } _liveSpeciesPlaceTokens = tokens; _liveSpeciesPlaceTokensCount = count; } catch { // Keep prior cache if any. } } private static void Add(HashSet tokens, string value) { if (!string.IsNullOrEmpty(value)) { tokens.Add(value.Trim()); } } }