using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; namespace IdleSpectator; public sealed class ActivityStatusAuditResult { public bool Passed; public string Detail = ""; public int Total; public int MissingAuthored; public int MissingIcon; public int LocaleLeaks; public int EmptyPhrase; public int OrphanAuthored; public int MissingInterest; public int OrphanInterest; } /// /// Exhaustive live-library audit for status Activity prose, icons, and interest policy. /// public static class ActivityStatusHarness { public static ActivityStatusAuditResult RunAudit(string outputDirectory) { List liveIds = ActivityAssetCatalog.EnumerateLiveStatusIds(); var authored = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string id in ActivityStatusProse.AuthoredIds) { authored.Add(id); } int missingAuthored = 0; int missingIcon = 0; int localeLeaks = 0; int emptyPhrase = 0; int orphanAuthored = 0; int missingInterest = 0; int orphanInterest = 0; var tsv = new StringBuilder(); tsv.AppendLine( "id\tgain\tloss\tauthored\tinterest\tevent_strength\tcreates_interest\ticon\tlocale_leak\tduration\tallow_timer_reset\tnotes"); var liveSet = new HashSet(StringComparer.OrdinalIgnoreCase); var interestAuthored = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string id in EventCatalog.Status.AuthoredIds) { interestAuthored.Add(id); } for (int i = 0; i < liveIds.Count; i++) { string id = liveIds[i]; liveSet.Add(id); StatusAsset asset = ActivityAssetCatalog.TryGetStatusAsset(id); string gain = ActivityStatusProse.Phrase(id, gained: true, out bool authoredGain); string loss = ActivityStatusProse.Phrase(id, gained: false, out bool authoredLoss); bool hasAuthored = authoredGain && authoredLoss && ActivityStatusProse.HasAuthored(id); if (!hasAuthored) { missingAuthored++; } StatusInterestEntry interest = EventCatalog.Status.GetOrFallback(id); bool hasInterest = EventCatalog.Status.HasAuthored(id) && !interest.IsFallback; if (!hasInterest) { missingInterest++; } if (string.IsNullOrEmpty(gain) || string.IsNullOrEmpty(loss)) { emptyPhrase++; } Sprite icon = HudIcons.FromStatusId(id) ?? HudIcons.FromUiIcon("iconConfused") ?? HudIcons.Task(); bool iconOk = icon != null; if (!iconOk) { missingIcon++; } bool leak = LooksLikeLocaleLeak(gain, id) || LooksLikeLocaleLeak(loss, id); if (leak) { localeLeaks++; } string duration = ""; string allowReset = ""; try { if (asset != null) { duration = asset.duration.ToString("0.##"); allowReset = asset.allow_timer_reset ? "1" : "0"; } } catch { // ignore } string notes = "ok"; if (!hasAuthored) { notes = "missing_authored"; } else if (!hasInterest) { notes = "missing_interest"; } else if (!iconOk) { notes = "missing_icon"; } else if (leak) { notes = "locale_leak"; } else if (string.IsNullOrEmpty(gain) || string.IsNullOrEmpty(loss)) { notes = "empty_phrase"; } tsv.Append(id).Append('\t') .Append(Escape(gain)).Append('\t') .Append(Escape(loss)).Append('\t') .Append(hasAuthored ? "1" : "0").Append('\t') .Append(hasInterest ? "1" : "0").Append('\t') .Append(interest.EventStrength.ToString("0.#")).Append('\t') .Append(interest.CreatesInterest ? "1" : "0").Append('\t') .Append(iconOk ? "1" : "0").Append('\t') .Append(leak ? "1" : "0").Append('\t') .Append(duration).Append('\t') .Append(allowReset).Append('\t') .Append(notes).AppendLine(); string gainKey = ActivityStatusProse.TaskKey(id, true); string lossKey = ActivityStatusProse.TaskKey(id, false); if (string.IsNullOrEmpty(gainKey) || string.IsNullOrEmpty(lossKey)) { emptyPhrase++; } } foreach (string id in authored) { if (!liveSet.Contains(id)) { orphanAuthored++; tsv.Append(id).Append("\t\t\t1\t0\t0\t0\t0\t0\t\t\torphan_authored").AppendLine(); } } foreach (string id in interestAuthored) { if (!liveSet.Contains(id)) { orphanInterest++; tsv.Append(id).Append("\t\t\t0\t1\t0\t0\t0\t0\t\t\torphan_interest").AppendLine(); } } try { if (!string.IsNullOrEmpty(outputDirectory)) { Directory.CreateDirectory(outputDirectory); File.WriteAllText( Path.Combine(outputDirectory, "activity-status-audit.tsv"), tsv.ToString(), Encoding.UTF8); } } catch { // Artifact write is best-effort. } int cameraDemotionFailures = 0; // Brief FX / cooldowns must stay B. Story-readable statuses must stay A. string[] bOnlyStatus = { "afterglow", "cough", "dash", "dodge", "flicked", "just_ate", "on_guard", "recovery_combat_action", "recovery_plot", "recovery_social", "recovery_spell", "shield", "slowness", "spell_boost", "spell_silence", "egg" }; for (int i = 0; i < bOnlyStatus.Length; i++) { StatusInterestEntry e = EventCatalog.Status.GetOrFallback(bOnlyStatus[i]); if (e.CreatesInterest) { cameraDemotionFailures++; } } string[] aStatus = { "drowning", "burning", "frozen", "poisoned", "possessed", "cursed", "pregnant", "crying", "rage", "stunned", "soul_harvested", "tantrum", "fell_in_love", "starving", "angry", "sleeping", "inspired", "ash_fever" }; for (int i = 0; i < aStatus.Length; i++) { StatusInterestEntry e = EventCatalog.Status.GetOrFallback(aStatus[i]); if (!e.CreatesInterest) { cameraDemotionFailures++; } } bool passed = liveIds.Count > 20 && missingAuthored == 0 && missingIcon == 0 && localeLeaks == 0 && emptyPhrase == 0 && orphanAuthored == 0 && missingInterest == 0 && orphanInterest == 0 && cameraDemotionFailures == 0; return new ActivityStatusAuditResult { Passed = passed, Total = liveIds.Count, MissingAuthored = missingAuthored, MissingIcon = missingIcon, LocaleLeaks = localeLeaks, EmptyPhrase = emptyPhrase, OrphanAuthored = orphanAuthored, MissingInterest = missingInterest, OrphanInterest = orphanInterest, Detail = $"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} " + $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} " + $"missingInterest={missingInterest} orphanInterest={orphanInterest} " + $"cameraDemotionFailures={cameraDemotionFailures} passed={passed}" }; } private static bool LooksLikeLocaleLeak(string phrase, string statusId) { if (string.IsNullOrEmpty(phrase)) { return true; } if (phrase.IndexOf("status_", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (!string.IsNullOrEmpty(statusId) && statusId.IndexOf('_') >= 0 && phrase.IndexOf(statusId, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } return false; } private static string Escape(string value) { if (string.IsNullOrEmpty(value)) { return ""; } return value.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); } }