worldbox-observer-mod/IdleSpectator/ActivityStatusHarness.cs

279 lines
9.1 KiB
C#

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;
}
/// <summary>
/// Exhaustive live-library audit for status Activity prose, icons, and interest policy.
/// </summary>
public static class ActivityStatusHarness
{
public static ActivityStatusAuditResult RunAudit(string outputDirectory)
{
List<string> liveIds = ActivityAssetCatalog.EnumerateLiveStatusIds();
var authored = new HashSet<string>(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<string>(StringComparer.OrdinalIgnoreCase);
var interestAuthored = new HashSet<string>(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', ' ');
}
}