339 lines
11 KiB
C#
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', ' ');
|
|
}
|
|
}
|