687 lines
18 KiB
C#
687 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>Live asset/task lookup and species-token hygiene. No voice classification.</summary>
|
|
public static class ActivityAssetCatalog
|
|
{
|
|
private static HashSet<string> _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());
|
|
}
|
|
|
|
/// <summary>Title Case species for dossier nametag: <c>Unicorn</c>, not <c>unicorn</c>.</summary>
|
|
public static string SpeciesNametagLabel(string speciesId)
|
|
{
|
|
string raw = SpeciesDisplayLabel(speciesId);
|
|
return TitleCaseWords(string.IsNullOrEmpty(raw) ? speciesId : raw);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Live prefixes like <c>gatherer_*</c> where ≥2 jobs share <c>P_*</c> and bare <c>P</c>
|
|
/// is not itself a job. Rebuilt from <see cref="EnumerateLiveCitizenJobIds"/>.
|
|
/// </summary>
|
|
private static HashSet<string> _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<string> EnumerateLiveCitizenJobIds()
|
|
{
|
|
var ids = new List<string>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Live citizen-job display for dossier from <c>citizen_job_library</c> ids.
|
|
/// Resource-role families discovered from the library
|
|
/// (<c>gatherer_herbs</c> → <c>Herb Gatherer</c>); everything else is Title Case id
|
|
/// (<c>miner_deposit</c> → <c>Miner Deposit</c>). Never probes invented locale keys.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Title Case + discovered resource-role reorder for a citizen job id.
|
|
/// Public for harness audits over the full live library.
|
|
/// </summary>
|
|
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<string> ids = EnumerateLiveCitizenJobIds();
|
|
if (_resourceRoleJobPrefixes != null && _resourceRoleJobPrefixesCount == ids.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var idSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var prefixCounts = new Dictionary<string, int>(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<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (KeyValuePair<string, int> 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;
|
|
}
|
|
|
|
/// <summary>Title-case whitespace-separated words for nametag identity tags.</summary>
|
|
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<string> EnumerateLiveActorAssetIds()
|
|
{
|
|
var ids = new List<string>();
|
|
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<string> EnumerateLiveStatusIds()
|
|
{
|
|
var ids = new List<string>();
|
|
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<string> EnumerateLiveHappinessIds()
|
|
{
|
|
var ids = new List<string>();
|
|
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<string> EnumerateLiveTaskIds()
|
|
{
|
|
return EnumerateLibraryIds("tasks_actor");
|
|
}
|
|
|
|
public static List<string> 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<string> 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<string> 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<string> EnumerateLivePlotIds()
|
|
{
|
|
return EnumerateLibraryIds("plots_library");
|
|
}
|
|
|
|
public static List<string> EnumerateLiveEraIds()
|
|
{
|
|
return EnumerateLibraryIds("era_library");
|
|
}
|
|
|
|
public static List<string> EnumerateLiveBookTypeIds()
|
|
{
|
|
return EnumerateLibraryIds("book_types");
|
|
}
|
|
|
|
public static List<string> EnumerateLiveTraitIds()
|
|
{
|
|
return EnumerateLibraryIds("traits");
|
|
}
|
|
|
|
public static List<string> EnumerateLiveBuildingIds()
|
|
{
|
|
return EnumerateLibraryIds("buildings");
|
|
}
|
|
|
|
public static List<string> EnumerateLiveSpellIds() => EnumerateLibraryIds("spells");
|
|
|
|
public static List<string> EnumerateLivePowerIds() => EnumerateLibraryIds("powers");
|
|
|
|
public static List<string> EnumerateLiveDecisionIds() => EnumerateLibraryIds("decisions_library");
|
|
|
|
public static List<string> EnumerateLiveItemIds() => EnumerateLibraryIds("items");
|
|
|
|
public static List<string> EnumerateLiveSubspeciesTraitIds() => EnumerateLibraryIds("subspecies_traits");
|
|
|
|
public static List<string> EnumerateLiveGeneIds() => EnumerateLibraryIds("gene_library");
|
|
|
|
public static List<string> EnumerateLivePhenotypeIds() => EnumerateLibraryIds("phenotype_library");
|
|
|
|
public static List<string> EnumerateLiveWorldLawIds() => EnumerateLibraryIds("world_laws_library");
|
|
|
|
public static List<string> EnumerateLiveBiomeIds() => EnumerateLibraryIds("biome_library");
|
|
|
|
public static List<string> EnumerateLiveCultureTraitIds() => EnumerateLibraryIds("culture_traits");
|
|
|
|
public static List<string> EnumerateLiveReligionTraitIds() => EnumerateLibraryIds("religion_traits");
|
|
|
|
public static List<string> EnumerateLiveClanTraitIds() => EnumerateLibraryIds("clan_traits");
|
|
|
|
public static List<string> EnumerateLiveLanguageTraitIds() => EnumerateLibraryIds("language_traits");
|
|
|
|
private static List<string> EnumerateLibraryIds(string assetManagerField)
|
|
{
|
|
var ids = new List<string>();
|
|
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<string>(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<string> tokens, string value)
|
|
{
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
tokens.Add(value.Trim());
|
|
}
|
|
}
|
|
}
|