468 lines
17 KiB
C#
468 lines
17 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"
|
|
};
|
|
|
|
private static readonly string[] UngroundedTerrainFragments =
|
|
{
|
|
"through the water",
|
|
"in the water",
|
|
"through the air",
|
|
"across the ground",
|
|
"over uneven ground",
|
|
"from above",
|
|
"from cover",
|
|
"close to the ground",
|
|
"nose to the ground",
|
|
"damp play",
|
|
"wet quiet"
|
|
};
|
|
|
|
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;
|
|
int terrainLeaks = 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);
|
|
}
|
|
string terrainLeak = voice.AssetKind == ActivityAssetKind.Vehicle
|
|
? ""
|
|
: FindPhrase(fingerprint, UngroundedTerrainFragments);
|
|
if (!string.IsNullOrEmpty(terrainLeak))
|
|
{
|
|
terrainLeaks++;
|
|
AddSample(ref sample, id + ":terrain=" + terrainLeak);
|
|
}
|
|
|
|
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
|
|
&& terrainLeaks == 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
|
|
+ " terrainLeaks=" + terrainLeaks
|
|
+ " pairFail=" + pairFail
|
|
+ " liveContextOk=" + liveContextOk
|
|
+ " sample='" + sample + "'"
|
|
};
|
|
}
|
|
|
|
private static string FindAwkwardPhrase(string fingerprint)
|
|
{
|
|
return FindPhrase(fingerprint, AwkwardProseFragments);
|
|
}
|
|
|
|
private static string FindPhrase(string fingerprint, string[] phrases)
|
|
{
|
|
string text = fingerprint ?? "";
|
|
for (int i = 0; i < phrases.Length; i++)
|
|
{
|
|
string phrase = phrases[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");
|
|
ActivityTerrain expectedTerrain = actor.current_tile?.Type == null
|
|
? ActivityTerrain.Unknown
|
|
: actor.current_tile.Type.liquid
|
|
? ActivityTerrain.Liquid
|
|
: actor.current_tile.Type.ground
|
|
? ActivityTerrain.Land
|
|
: ActivityTerrain.Unknown;
|
|
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
|
|
&& ctx.Terrain == expectedTerrain
|
|
&& !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;
|
|
}
|
|
}
|