using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; namespace IdleSpectator; public sealed class ActivityHappinessAuditResult { 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 MissingPolicy; public int RequiredContextFailures; public int MissingEventStrength; public int ProseRegressions; } /// /// Exhaustive live-library audit for happiness Activity prose, icons, and policies. /// public static class ActivityHappinessHarness { public static ActivityHappinessAuditResult RunAudit(string outputDirectory) { List liveIds = ActivityAssetCatalog.EnumerateLiveHappinessIds(); var authored = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string id in EventCatalog.Happiness.AuthoredIds) { authored.Add(id); } int missingAuthored = 0; int missingIcon = 0; int localeLeaks = 0; int emptyPhrase = 0; int orphanAuthored = 0; int missingPolicy = 0; int requiredContextFailures = 0; int missingEventStrength = 0; int proseRegressions = 0; var tsv = new StringBuilder(); tsv.AppendLine( "id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tevent_strength\tclause\tauthored\ticon\tlocale_leak\tnotes"); var liveSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < liveIds.Count; i++) { string id = liveIds[i]; liveSet.Add(id); HappinessAsset asset = ActivityAssetCatalog.TryGetHappinessAsset(id); bool hasAuthored = EventCatalog.Happiness.HasAuthored(id); HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id); if (!hasAuthored || entry.IsFallback) { missingAuthored++; } if (hasAuthored && entry.EventStrength <= 0f) { missingEventStrength++; } string clauseWith = ActivityHappinessProse.Clause(entry, "Ava", out bool usedRelated, out _); string clauseWithout = ActivityHappinessProse.Clause(entry, "", out _, out _); if (string.IsNullOrEmpty(clauseWith) || string.IsNullOrEmpty(clauseWithout)) { emptyPhrase++; } if (entry.Context == HappinessContextRequirement.RequiresRelatedActor) { if (!usedRelated || clauseWith.IndexOf("Ava", StringComparison.Ordinal) < 0) { requiredContextFailures++; } if (clauseWith.IndexOf("{related}", StringComparison.Ordinal) >= 0 || clauseWithout.IndexOf("{related}", StringComparison.Ordinal) >= 0) { requiredContextFailures++; } } if (entry.Presentation == 0 && entry.Life == 0 && string.IsNullOrEmpty(entry.InterestCategory)) { missingPolicy++; } if (string.IsNullOrEmpty(entry.InterestCategory) || entry.VariantsWithoutRelated == null || entry.VariantsWithoutRelated.Length == 0) { missingPolicy++; } Sprite icon = HudIcons.FromHappinessId(id) ?? HudIcons.FromUiIcon("iconHappy") ?? HudIcons.Task(); bool iconOk = icon != null; if (!iconOk) { missingIcon++; } bool leak = LooksLikeLocaleLeak(clauseWith, id) || LooksLikeLocaleLeak(clauseWithout, id); if (leak) { localeLeaks++; } int value = 0; string psycho = ""; try { if (asset != null) { value = asset.value; psycho = asset.ignored_by_psychopaths ? "1" : "0"; } } catch { // ignore } string notes = "ok"; if (!hasAuthored || entry.IsFallback) { notes = "missing_authored"; } else if (entry.EventStrength <= 0f) { notes = "missing_event_strength"; } else if (!iconOk) { notes = "missing_icon"; } else if (leak) { notes = "locale_leak"; } else if (string.IsNullOrEmpty(clauseWith) || string.IsNullOrEmpty(clauseWithout)) { notes = "empty_phrase"; } else if (entry.Context == HappinessContextRequirement.RequiresRelatedActor && (!usedRelated || clauseWith.IndexOf("Ava", StringComparison.Ordinal) < 0)) { notes = "required_context"; } tsv.Append(id).Append('\t') .Append(value).Append('\t') .Append(psycho).Append('\t') .Append(entry.Presentation).Append('\t') .Append(entry.Life).Append('\t') .Append(entry.Context).Append('\t') .Append(entry.Overlap).Append('\t') .Append(entry.EventStrength.ToString("0.#")).Append('\t') .Append(Escape(clauseWith)).Append('\t') .Append(hasAuthored ? "1" : "0").Append('\t') .Append(iconOk ? "1" : "0").Append('\t') .Append(leak ? "1" : "0").Append('\t') .Append(notes).AppendLine(); } foreach (string id in authored) { if (!liveSet.Contains(id)) { orphanAuthored++; tsv.Append(id).Append('\t') .Append("\t\t\t\t\t\t\t0\t\t1\t0\t0\torphan_authored").AppendLine(); } } try { if (!string.IsNullOrEmpty(outputDirectory)) { Directory.CreateDirectory(outputDirectory); File.WriteAllText( Path.Combine(outputDirectory, "activity-happiness-audit.tsv"), tsv.ToString()); } } catch { // Disk may be unavailable in some harness contexts. } // Prose vs call-site regressions (Layer B honesty). proseRegressions += CountProseRegression( "just_born", mustContain: new[] { "appears in the world", "brought into existence" }, mustNotContain: new[] { "born into the world", "first breath" }); proseRegressions += CountProseRegression( "just_kissed", mustContain: new[] { "mates", "intimacy" }, mustNotContain: new[] { "kiss" }); proseRegressions += CountProseRegression( "just_got_out_of_egg", mustContain: new[] { "hatches from an egg" }, mustNotContain: Array.Empty()); // Camera demotion: only true FX / bookkeeping stay Ambient (Layer B). // Interesting life/social beats are Signal; director ranks by EventStrength. string[] ambientRequired = { "got_poked", "got_caught", "paid_tax", "just_ate", "just_pooped" }; for (int i = 0; i < ambientRequired.Length; i++) { HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(ambientRequired[i]); if (e.Presentation == HappinessPresentationTier.Signal) { proseRegressions++; } } string[] signalRequired = { "just_made_friend", "just_injured", "just_found_house", "got_robbed", "fallen_in_love", "become_alpha", "just_had_child" }; for (int i = 0; i < signalRequired.Length; i++) { HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(signalRequired[i]); if (e.Presentation != HappinessPresentationTier.Signal) { proseRegressions++; } } bool passed = missingAuthored == 0 && missingIcon == 0 && localeLeaks == 0 && emptyPhrase == 0 && orphanAuthored == 0 && missingPolicy == 0 && requiredContextFailures == 0 && missingEventStrength == 0 && proseRegressions == 0; return new ActivityHappinessAuditResult { Passed = passed, Total = liveIds.Count, MissingAuthored = missingAuthored, MissingIcon = missingIcon, LocaleLeaks = localeLeaks, EmptyPhrase = emptyPhrase, OrphanAuthored = orphanAuthored, MissingPolicy = missingPolicy, RequiredContextFailures = requiredContextFailures, MissingEventStrength = missingEventStrength, ProseRegressions = proseRegressions, Detail = $"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} " + $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} " + $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures} " + $"missingEventStrength={missingEventStrength} proseRegressions={proseRegressions}" }; } private static int CountProseRegression(string id, string[] mustContain, string[] mustNotContain) { HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id); if (entry == null || entry.IsFallback) { return 1; } string joined = string.Join(" ", entry.VariantsWithoutRelated ?? Array.Empty()) + " " + string.Join(" ", entry.VariantsWithRelated ?? Array.Empty()); for (int i = 0; mustContain != null && i < mustContain.Length; i++) { if (joined.IndexOf(mustContain[i], StringComparison.OrdinalIgnoreCase) < 0) { return 1; } } for (int i = 0; mustNotContain != null && i < mustNotContain.Length; i++) { if (joined.IndexOf(mustNotContain[i], StringComparison.OrdinalIgnoreCase) >= 0) { return 1; } } return 0; } private static bool LooksLikeLocaleLeak(string text, string id) { if (string.IsNullOrEmpty(text)) { return true; } if (text.IndexOf("happiness_", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (!string.IsNullOrEmpty(id) && text.Equals(id, StringComparison.OrdinalIgnoreCase)) { return true; } if (text.IndexOf('{') >= 0 || text.IndexOf('}') >= 0) { return true; } return false; } private static string Escape(string text) { if (string.IsNullOrEmpty(text)) { return ""; } return text.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); } }