worldbox-observer-mod/IdleSpectator/ActivityHappinessHarness.cs

339 lines
11 KiB
C#

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;
}
/// <summary>
/// Exhaustive live-library audit for happiness Activity prose, icons, and policies.
/// </summary>
public static class ActivityHappinessHarness
{
public static ActivityHappinessAuditResult RunAudit(string outputDirectory)
{
List<string> liveIds = ActivityAssetCatalog.EnumerateLiveHappinessIds();
var authored = new HashSet<string>(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<string>(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<string>());
// 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>())
+ " "
+ string.Join(" ", entry.VariantsWithRelated ?? Array.Empty<string>());
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', ' ');
}
}