worldbox-observer-mod/IdleSpectator/ActivityVoiceHarness.cs

429 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace IdleSpectator;
public sealed class ActivityVoiceAuditResult
{
public bool Passed;
public string Detail = "";
}
/// <summary>Exhaustive live-library gates for voice classification and prose fingerprints.</summary>
public static class ActivityVoiceHarness
{
private static readonly string[] FingerprintKeys =
{
"move", "wait", "task_unit_play", "task_eat", "task_sleep",
"task_hunt", "task_attack", "social", "laugh", "work"
};
private static readonly string[] AwkwardProseFragments =
{
"under combat pressure",
"through the living world",
"through the living moment",
"in a living rush",
"living energy",
"proper meal",
"focused appetite",
"careful focus",
"constructed steps",
"spores about",
"gregs onward",
"through the wet",
"quiet purpose",
"deathly little steps",
"mythic calm",
"otherworldly purpose",
"patient citrus"
};
public static ActivityVoiceAuditResult RunAudit(string outputDirectory)
{
List<string> ids = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
var rows = new StringBuilder(32768);
rows.AppendLine("id\tbase\tkind\tmodifier\tfamily\tbody\teffective\tecology\tordinal\trecovery\tfingerprint");
int missing = 0;
int defaults = 0;
int emptyProse = 0;
int leaks = 0;
int recoveryFail = 0;
int modifierFail = 0;
int aquaticFalsePositive = 0;
int collisions = 0;
int vehicleMismatch = 0;
int awkwardProse = 0;
string sample = "";
string vehicleFingerprint = null;
var fingerprints = new Dictionary<string, string>(StringComparer.Ordinal);
for (int i = 0; i < ids.Count; i++)
{
string id = ids[i];
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(id);
if (asset == null)
{
missing++;
AddSample(ref sample, id + ":missing");
continue;
}
ProseVoice voice = ActivityVoiceResolver.Resolve(asset);
if (voice.EffectiveActionTag == ActivityBodyTag.Default
|| string.IsNullOrEmpty(voice.BaseFamily)
|| voice.BaseFamily == ActivityVoiceFamilies.Default)
{
defaults++;
AddSample(ref sample, id + ":default");
}
if (voice.BaseRecoveryExpected && !voice.BaseRecoverySucceeded)
{
recoveryFail++;
AddSample(ref sample, id + ":base?");
}
if (!ExpectedModifierMatches(asset, voice))
{
modifierFail++;
AddSample(ref sample, id + ":modifier=" + voice.Modifier);
}
if (MustNotBeAquatic(id)
&& (voice.BaseBodyTag == ActivityBodyTag.Aquatic
|| voice.EcologyTag == ActivityEcologyTag.Water
&& voice.AssetKind != ActivityAssetKind.Vehicle))
{
aquaticFalsePositive++;
AddSample(ref sample, id + ":aquatic");
}
int localEmpty;
int localLeaks;
string fingerprint = Fingerprint(id, out localEmpty, out localLeaks);
emptyProse += localEmpty;
leaks += localLeaks;
string awkward = FindAwkwardPhrase(fingerprint);
if (!string.IsNullOrEmpty(awkward))
{
awkwardProse++;
AddSample(ref sample, id + ":awkward=" + awkward);
}
if (voice.AssetKind == ActivityAssetKind.Vehicle)
{
if (vehicleFingerprint == null)
{
vehicleFingerprint = fingerprint;
}
else if (!vehicleFingerprint.Equals(fingerprint, StringComparison.Ordinal))
{
vehicleMismatch++;
AddSample(ref sample, id + ":vehicle-fingerprint");
}
}
else if (fingerprints.TryGetValue(fingerprint, out string prior))
{
collisions++;
AddSample(ref sample, prior + "=" + id);
}
else
{
fingerprints[fingerprint] = id;
}
rows.Append(id).Append('\t')
.Append(voice.BaseSpeciesId).Append('\t')
.Append(voice.AssetKind).Append('\t')
.Append(voice.Modifier).Append('\t')
.Append(voice.BaseFamily).Append('\t')
.Append(voice.BaseBodyTag).Append('\t')
.Append(voice.EffectiveActionTag).Append('\t')
.Append(voice.EcologyTag).Append('\t')
.Append(voice.FlavorOrdinal).Append('\t')
.Append(voice.BaseRecoveryExpected ? voice.BaseRecoverySucceeded.ToString() : "n/a").Append('\t')
.Append(fingerprint.Replace('\t', ' ')).Append('\n');
}
int pairFail = PairwiseFailures(ref sample);
bool liveContextOk = VerifyLiveContext(ref sample);
try
{
Directory.CreateDirectory(outputDirectory);
File.WriteAllText(Path.Combine(outputDirectory, "activity-voice-audit.tsv"), rows.ToString());
}
catch (Exception ex)
{
AddSample(ref sample, "write:" + ex.Message);
}
bool passed = ids.Count > 50
&& missing == 0
&& defaults == 0
&& emptyProse == 0
&& leaks == 0
&& recoveryFail == 0
&& modifierFail == 0
&& aquaticFalsePositive == 0
&& collisions == 0
&& vehicleMismatch == 0
&& awkwardProse == 0
&& pairFail == 0
&& liveContextOk;
return new ActivityVoiceAuditResult
{
Passed = passed,
Detail = "total=" + ids.Count
+ " missing=" + missing
+ " defaults=" + defaults
+ " empty=" + emptyProse
+ " leaks=" + leaks
+ " recoveryFail=" + recoveryFail
+ " modifierFail=" + modifierFail
+ " aquaticFalsePositive=" + aquaticFalsePositive
+ " collisions=" + collisions
+ " vehicleMismatch=" + vehicleMismatch
+ " awkwardProse=" + awkwardProse
+ " pairFail=" + pairFail
+ " liveContextOk=" + liveContextOk
+ " sample='" + sample + "'"
};
}
private static string FindAwkwardPhrase(string fingerprint)
{
string text = fingerprint ?? "";
for (int i = 0; i < AwkwardProseFragments.Length; i++)
{
string phrase = AwkwardProseFragments[i];
if (text.IndexOf(phrase, StringComparison.OrdinalIgnoreCase) >= 0)
{
return phrase;
}
}
return "";
}
private static bool VerifyLiveContext(ref string sample)
{
try
{
Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (actor == null || actor.asset == null)
{
AddSample(ref sample, "live-context:no-focus");
return false;
}
ActivityContext ctx = ActivityInterestTable.BuildContext(actor, "move");
ActivityProse.Format(
ActivityKind.TaskStart,
"move",
ctx,
"Moving",
actor.getID(),
1,
out string line,
out _);
bool ok = ctx.Voice != null
&& ctx.Voice.LiveSpeciesId == (actor.asset.id ?? "")
&& ctx.Voice.EffectiveActionTag != ActivityBodyTag.Default
&& !string.IsNullOrEmpty(line);
if (!ok)
{
AddSample(ref sample, "live-context:" + (actor.asset.id ?? "") + ":" + line);
}
return ok;
}
catch (Exception ex)
{
AddSample(ref sample, "live-context:" + ex.Message);
return false;
}
}
public static string Fingerprint(string speciesId, out int empty, out int leaks)
{
empty = 0;
leaks = 0;
var corpus = new StringBuilder();
ActivityContext probe = ActivityContext.ForHarness("VoiceActor", "VoiceTarget", speciesId);
string speciesLabel = probe.SpeciesLabel ?? "";
for (int i = 0; i < FingerprintKeys.Length; i++)
{
string key = FingerprintKeys[i];
bool target = key == "task_hunt" || key == "task_attack" || key == "social";
ActivityContext ctx = ActivityContext.ForHarness(
"VoiceActor",
target ? "VoiceTarget" : "",
speciesId,
place: "VoicePlace",
carrying: "VoiceCargo",
jobId: "VoiceJob",
taskKey: key,
traitId: "VoiceTrait");
ActivityProse.Format(
ActivityKind.TaskStart,
key,
ctx,
key,
4242,
i + 1,
out string line,
out _);
if (string.IsNullOrEmpty(line))
{
empty++;
}
if (ContainsSpeciesOrTrait(line, speciesId, speciesLabel, "VoiceTrait"))
{
leaks++;
}
corpus.Append(Normalize(line, speciesId, speciesLabel)).Append('|');
}
return corpus.ToString();
}
private static int PairwiseFailures(ref string sample)
{
string[,] pairs =
{
{ "human", "zombie_human" },
{ "dog", "zombie_animal_dog" },
{ "lemon_snail", "zombie_animal_lemon_snail" },
{ "dragon", "zombie_dragon" },
{ "fairy", "zombie_animal_fairy" },
{ "fire_elemental_horse", "fire_elemental_snake" }
};
int failures = 0;
for (int i = 0; i < pairs.GetLength(0); i++)
{
string a = pairs[i, 0];
string b = pairs[i, 1];
if (!ActivityAssetCatalog.IsLiveActorAssetId(a)
|| !ActivityAssetCatalog.IsLiveActorAssetId(b))
{
failures++;
AddSample(ref sample, a + "/" + b + ":missing-pair");
continue;
}
string fa = Fingerprint(a, out _, out _);
string fb = Fingerprint(b, out _, out _);
if (fa.Equals(fb, StringComparison.Ordinal))
{
failures++;
AddSample(ref sample, a + "=" + b);
}
}
List<string> ids = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
string boat = null;
for (int i = 0; i < ids.Count; i++)
{
if (!ids[i].StartsWith("boat_", StringComparison.OrdinalIgnoreCase)) continue;
string fp = Fingerprint(ids[i], out _, out _);
if (boat == null) boat = fp;
else if (!boat.Equals(fp, StringComparison.Ordinal))
{
failures++;
AddSample(ref sample, ids[i] + ":boat-pair");
break;
}
}
return failures;
}
private static string Normalize(string line, string speciesId, string speciesLabel)
{
string text = Regex.Replace(line ?? "", "<[^>]+>", "");
text = ReplaceIgnoreCase(text, "VoiceActor", "");
text = ReplaceIgnoreCase(text, "VoiceTarget", "");
text = ReplaceIgnoreCase(text, "VoicePlace", "");
text = ReplaceIgnoreCase(text, "VoiceCargo", "");
text = ReplaceIgnoreCase(text, "VoiceJob", "");
text = ReplaceIgnoreCase(text, "VoiceTrait", "");
text = ReplaceIgnoreCase(text, speciesLabel, "");
text = ReplaceIgnoreCase(text, (speciesId ?? "").Replace('_', ' '), "");
text = Regex.Replace(text.ToLowerInvariant(), "[^a-z0-9]+", " ").Trim();
return text;
}
private static bool ContainsSpeciesOrTrait(
string line,
string speciesId,
string speciesLabel,
string trait)
{
if (string.IsNullOrEmpty(line)) return false;
if (!string.IsNullOrEmpty(trait)
&& line.IndexOf(trait, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (!string.IsNullOrEmpty(speciesLabel)
&& line.IndexOf(" the " + speciesLabel, StringComparison.OrdinalIgnoreCase) >= 0) return true;
string idWords = (speciesId ?? "").Replace('_', ' ');
return !string.IsNullOrEmpty(idWords)
&& line.IndexOf(" the " + idWords, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool ExpectedModifierMatches(ActorAsset asset, ProseVoice voice)
{
string id = asset.id ?? "";
string wild = asset.kingdom_id_wild ?? "";
if (id.StartsWith("boat_", StringComparison.OrdinalIgnoreCase))
return voice.AssetKind == ActivityAssetKind.Vehicle;
if (wild.Equals("undead", StringComparison.OrdinalIgnoreCase)
|| id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Undead;
if (wild.Equals("fire_elemental", StringComparison.OrdinalIgnoreCase)
|| id.StartsWith("fire_elemental", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Elemental;
if (id.StartsWith("mush_", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Mush;
if (id.StartsWith("tumor_", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Tumor;
if (wild.Equals("aliens", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Alien;
if (wild.Equals("living_houses", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Construct;
if (id.Equals("assimilator", StringComparison.OrdinalIgnoreCase)
|| id.Equals("printer", StringComparison.OrdinalIgnoreCase)
|| id.Equals("god_finger", StringComparison.OrdinalIgnoreCase)
|| id.Equals("crystal_sword", StringComparison.OrdinalIgnoreCase))
return voice.Modifier == ActivityModifier.Construct;
return true;
}
private static bool MustNotBeAquatic(string id)
{
return id.StartsWith("ant_", StringComparison.OrdinalIgnoreCase)
|| id.IndexOf("spider", StringComparison.OrdinalIgnoreCase) >= 0
|| id.Equals("worm", StringComparison.OrdinalIgnoreCase)
|| id.Equals("printer", StringComparison.OrdinalIgnoreCase);
}
private static string ReplaceIgnoreCase(string text, string value, string replacement)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(value)) return text;
return Regex.Replace(text, Regex.Escape(value), replacement ?? "", RegexOptions.IgnoreCase);
}
private static void AddSample(ref string sample, string value)
{
if (sample.Length >= 500) return;
if (sample.Length > 0) sample += ";";
sample += value;
}
}