using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace IdleSpectator;
public sealed class EventInjectCoverageResult
{
public bool Passed;
public string Detail = "";
public int Attempted;
public int Registered;
public int SkippedB;
public int Failed;
}
///
/// Data-driven Layer A inject coverage: every camera-worthy catalog id must register
/// an interest key. New JSON/live-seeded ids join automatically via AuthoredIds.
/// Does not prove live Beh/API outcomes - see event_live_outcome_smoke for that track.
///
/// Each successful inject is removed from immediately so
/// trim cannot false-fail later ids.
///
public static class EventInjectCoverageHarness
{
public static EventInjectCoverageResult Run(string outputDirectory, Actor unit)
{
var result = new EventInjectCoverageResult();
if (unit == null || !unit.isAlive())
{
result.Detail = "no living unit for inject coverage";
return result;
}
InterestRegistry.Clear();
var tsv = new StringBuilder();
tsv.AppendLine("domain\tid\tcamera\tresult\tkey\tnotes");
var failures = new List(32);
void Row(string domain, string id, bool camera, string outcome, string key, string notes)
{
tsv.Append(domain).Append('\t')
.Append(id).Append('\t')
.Append(camera ? "true" : "false").Append('\t')
.Append(outcome).Append('\t')
.Append(key ?? "").Append('\t')
.Append(notes ?? "").Append('\n');
}
void Fail(string domain, string id, string notes)
{
result.Failed++;
failures.Add(domain + ":" + id + " " + notes);
Row(domain, id, true, "FAIL", "", notes);
}
void SkipB(string domain, string id, string notes)
{
result.SkippedB++;
Row(domain, id, false, "skip_b", "", notes);
}
void Ok(string domain, string id, string key)
{
result.Registered++;
Row(domain, id, true, "ok", key, "");
}
bool Accept(InterestCandidate c, string needle, out string key, out string notes)
{
key = "";
notes = "";
if (c == null || string.IsNullOrEmpty(c.Key))
{
notes = "register_returned_null";
return false;
}
key = c.Key;
if (!string.IsNullOrEmpty(needle)
&& c.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0)
{
notes = "key_mismatch want=" + needle + " got=" + c.Key;
return false;
}
// Drop immediately so MaxCandidates trim cannot wipe later injects.
InterestRegistry.Remove(c.Key);
return true;
}
// Happiness Signal
foreach (string id in EventCatalog.Happiness.AuthoredIds)
{
result.Attempted++;
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
SkipB("happiness", id, entry.Presentation.ToString());
continue;
}
string label = EventReason.Apply("{a} · " + id, unit, id: id);
InterestCandidate c = InterestFeeds.RegisterHappinessSignal(unit, id, label);
string needle = "happiness:" + id;
if (!Accept(c, needle, out string key, out string notes))
{
Fail("happiness", id, notes);
continue;
}
Ok("happiness", id, key);
}
// Status CreatesInterest (gains)
foreach (string id in EventCatalog.Status.AuthoredIds)
{
result.Attempted++;
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
SkipB("status", id, "CreatesInterest=false");
continue;
}
string phrase = ActivityStatusProse.Phrase(id, gained: true, out _);
string key = "status:" + id + ":" + EventFeedUtil.SafeId(unit);
InterestCandidate c = EventFeedUtil.Register(
key,
"status_inject",
string.IsNullOrEmpty(entry.Category) ? "StatusTransformation" : entry.Category,
EventReason.Status(unit, phrase),
id,
entry.EventStrength,
unit.current_position,
unit);
if (!Accept(c, "status:" + id, out string accepted, out string notes))
{
Fail("status", id, notes);
continue;
}
Ok("status", id, accepted);
}
// WorldLog
foreach (string id in EventCatalog.WorldLog.AuthoredIds)
{
result.Attempted++;
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
SkipB("worldLog", id, "CreatesInterest=false");
continue;
}
string key = "worldlog:" + id + ":inject";
string label = entry.MakeLabel("Alpha", "Beta");
InterestCandidate c = EventFeedUtil.Register(
key,
"worldlog_inject",
string.IsNullOrEmpty(entry.Category) ? "WorldLog" : entry.Category,
label,
id,
entry.EventStrength,
unit.current_position,
unit,
locationOnly: false);
if (!Accept(c, "worldlog:" + id, out string accepted, out string notes))
{
Fail("worldLog", id, notes);
continue;
}
Ok("worldLog", id, accepted);
}
// Disasters → paired WorldLog id (policy overlay; camera via worldLog)
foreach (string id in EventCatalog.Disaster.AuthoredIds)
{
result.Attempted++;
DisasterInterestEntry entry = EventCatalog.Disaster.GetOrFallback(id);
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
{
SkipB("disasters", id, "camera=false");
continue;
}
string worldLogId = string.IsNullOrEmpty(entry.WorldLogId)
? ("disaster_" + id)
: entry.WorldLogId;
WorldLogEventEntry wl = EventCatalog.WorldLog.GetOrFallback(worldLogId);
if (!EventCatalog.IsCameraWorthy(wl))
{
Fail("disasters", id, "paired_worldLog_not_camera:" + worldLogId);
continue;
}
string key = "worldlog:" + worldLogId + ":disaster_inject";
InterestCandidate c = EventFeedUtil.Register(
key,
"disaster_inject",
"Disaster",
wl.MakeLabel("Alpha", "Beta"),
worldLogId,
entry.EventStrength,
unit.current_position,
unit);
if (!Accept(c, "worldlog:" + worldLogId, out string accepted, out string notes))
{
Fail("disasters", id, notes + " worldLog=" + worldLogId);
continue;
}
Ok("disasters", id, accepted);
}
// War types
foreach (string id in EventCatalog.WarType.AuthoredIds)
{
result.Attempted++;
WarTypeInterestEntry entry = EventCatalog.WarType.GetOrFallback(id);
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
{
SkipB("warTypes", id, "camera=false");
continue;
}
string key = "war:inject:" + id;
string label = EventReason.Apply(entry.LabelTemplate, unit, id: id);
if (string.IsNullOrEmpty(label))
{
label = "War (" + id + ")";
}
InterestCandidate c = EventFeedUtil.Register(
key,
"war_inject",
string.IsNullOrEmpty(entry.Category) ? "War" : entry.Category,
label,
id,
entry.EventStrength,
unit.current_position,
unit);
if (!Accept(c, key, out string accepted, out string notes))
{
Fail("warTypes", id, notes);
continue;
}
Ok("warTypes", id, accepted);
}
InjectDiscreteDomain(
"relationship",
EventCatalog.Relationship.AuthoredIds,
id => EventCatalog.Relationship.GetOrFallback(id),
"rel",
unit,
result,
Fail,
SkipB,
Ok,
Accept);
InjectDiscreteDomain(
"plots",
EventCatalog.Plot.AuthoredIds,
id => EventCatalog.Plot.GetOrFallback(id),
"plot",
unit,
result,
Fail,
SkipB,
Ok,
Accept,
phaseAware: true);
InjectDiscreteDomain(
"books",
EventCatalog.Book.AuthoredIds,
id => EventCatalog.Book.GetOrFallback(id),
"book",
unit,
result,
Fail,
SkipB,
Ok,
Accept);
InjectDiscreteDomain(
"eras",
EventCatalog.Era.AuthoredIds,
id => EventCatalog.Era.GetOrFallback(id),
"era",
unit,
result,
Fail,
SkipB,
Ok,
Accept,
locationOnlyOk: true);
InjectDiscreteDomain(
"traits",
EventCatalog.Trait.AuthoredIds,
id => EventCatalog.Trait.GetOrFallback(id),
"trait",
unit,
result,
Fail,
SkipB,
Ok,
Accept,
useTraitLabel: true);
// Decisions: only camera-worthy Signal decisions (live-seeded + JSON)
foreach (string id in EventCatalog.Decision.AuthoredIds)
{
result.Attempted++;
if (!EventCatalog.Decision.IsCameraWorthy(id))
{
SkipB("decisions", id, "not_interesting_decision");
continue;
}
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
string key = "decision:" + id + ":" + EventFeedUtil.SafeId(unit);
string label = EventReason.Decision(unit, id);
InterestCandidate c = EventFeedUtil.Register(
key,
"decision_inject",
entry.Category,
label,
id,
entry.EventStrength,
unit.current_position,
unit);
if (!Accept(c, "decision:" + id, out string accepted, out string notes))
{
Fail("decisions", id, notes);
continue;
}
Ok("decisions", id, accepted);
}
// Libraries - camera-worthy only (Signal / ambientCreatesInterest policy)
InjectLibrary("spell", EventCatalog.Spell.AuthoredIds, id => EventCatalog.Spell.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("item", EventCatalog.Item.AuthoredIds, id => EventCatalog.Item.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("power", EventCatalog.Power.AuthoredIds, id => EventCatalog.Power.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("gene", EventCatalog.Gene.AuthoredIds, id => EventCatalog.Gene.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("phenotype", EventCatalog.Phenotype.AuthoredIds, id => EventCatalog.Phenotype.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("worldLaw", EventCatalog.WorldLaw.AuthoredIds, id => EventCatalog.WorldLaw.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("subspecies_trait", EventCatalog.SubspeciesTrait.AuthoredIds, id => EventCatalog.SubspeciesTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("biome", EventCatalog.Biome.AuthoredIds, id => EventCatalog.Biome.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("culture_trait", EventCatalog.CultureTrait.AuthoredIds, id => EventCatalog.CultureTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("religion_trait", EventCatalog.ReligionTrait.AuthoredIds, id => EventCatalog.ReligionTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("clan_trait", EventCatalog.ClanTrait.AuthoredIds, id => EventCatalog.ClanTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
InjectLibrary("language_trait", EventCatalog.LanguageTrait.AuthoredIds, id => EventCatalog.LanguageTrait.GetOrFallback(id), unit, result, Fail, SkipB, Ok, Accept);
CatalogCoverageAudit.WriteReport(outputDirectory, "event-inject-coverage.tsv", tsv.ToString());
result.Passed = result.Failed == 0 && result.Registered > 0;
result.Detail =
$"attempted={result.Attempted} registered={result.Registered} skip_b={result.SkippedB} "
+ $"failed={result.Failed}"
+ (failures.Count > 0
? (" first=" + failures[0] + (failures.Count > 1 ? " +" + (failures.Count - 1) : ""))
: "");
return result;
}
private delegate DiscreteEventEntry DiscreteGetter(string id);
private delegate bool AcceptFn(InterestCandidate c, string needle, out string key, out string notes);
private static void InjectDiscreteDomain(
string domain,
IEnumerable ids,
DiscreteGetter get,
string keyPrefix,
Actor unit,
EventInjectCoverageResult result,
Action fail,
Action skipB,
Action ok,
AcceptFn accept,
bool phaseAware = false,
bool locationOnlyOk = false,
bool useTraitLabel = false)
{
if (ids == null)
{
return;
}
foreach (string raw in ids)
{
result.Attempted++;
string id = (raw ?? "").Trim();
if (string.IsNullOrEmpty(id))
{
continue;
}
DiscreteEventEntry entry = get(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
skipB(domain, id, "CreatesInterest=false");
continue;
}
string key = keyPrefix + ":" + id
+ (phaseAware ? ":new:" : ":")
+ (locationOnlyOk ? "inject" : EventFeedUtil.SafeId(unit).ToString());
if (domain == "eras")
{
key = "era:" + id;
}
else if (domain == "traits")
{
key = "trait:" + id + ":gains:" + EventFeedUtil.SafeId(unit);
}
else if (domain == "relationship")
{
key = "rel:" + id + ":" + EventFeedUtil.SafeId(unit);
}
else if (domain == "plots")
{
key = "plot:" + id + ":new:" + EventFeedUtil.SafeId(unit);
}
else if (domain == "books")
{
key = "book:" + id + ":new:" + EventFeedUtil.SafeId(unit);
}
string label;
if (useTraitLabel)
{
label = EventReason.Trait(unit, id, gained: true);
}
else if (phaseAware && domain == "plots")
{
label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id);
}
else if (domain == "eras")
{
label = entry.MakeLabel("", "");
}
else
{
label = entry.MakeLabel(unit);
}
InterestCandidate c = EventFeedUtil.Register(
key,
domain + "_inject",
entry.Category,
label,
id,
entry.EventStrength,
unit.current_position,
follow: unit,
locationOnly: false);
string needle = keyPrefix == "rel" ? "rel:" + id : key.Split(':')[0] + ":" + id;
if (domain == "eras")
{
needle = "era:" + id;
}
if (!accept(c, needle, out string accepted, out string notes))
{
fail(domain, id, notes);
continue;
}
ok(domain, id, accepted);
}
}
private static void InjectLibrary(
string domain,
IEnumerable ids,
DiscreteGetter get,
Actor unit,
EventInjectCoverageResult result,
Action fail,
Action skipB,
Action ok,
AcceptFn accept)
{
if (ids == null)
{
return;
}
foreach (string raw in ids)
{
result.Attempted++;
string id = (raw ?? "").Trim();
if (string.IsNullOrEmpty(id))
{
continue;
}
DiscreteEventEntry entry = get(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
skipB(domain, id, "CreatesInterest=false");
continue;
}
string key = domain + ":" + id + ":" + EventFeedUtil.SafeId(unit);
string label = EventReason.Library(unit, domain, id);
InterestCandidate c = EventFeedUtil.Register(
key,
domain + "_inject",
entry.Category,
label,
id,
entry.EventStrength,
unit.current_position,
unit);
if (!accept(c, domain + ":" + id, out string accepted, out string notes))
{
fail(domain, id, notes);
continue;
}
ok(domain, id, accepted);
}
}
}