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); } } }