using System; using System.Collections.Generic; namespace IdleSpectator; public sealed class EventLiveApplyLoopResult { public bool Passed; public string Detail = ""; public int HappinessAttempted; public int StatusAttempted; public int Failures; } /// /// Phase 1 live apply loops: every camera-worthy happiness Signal and status gain /// through real changeHappiness / addStatusEffect. Claims into /// (id / id_drop / fail). /// Primary asserts are ActivityLog / router notes (Busy may skip interest). /// public static class EventLiveApplyLoopHarness { public static EventLiveApplyLoopResult Run(string outputDirectory, Actor unit) { var result = new EventLiveApplyLoopResult(); if (unit == null || !unit.isAlive()) { result.Detail = "no living unit for live apply loop"; return result; } EventLiveCoverageLedger.SeedFromCatalogs(); long subjectId = EventFeedUtil.SafeId(unit); var fails = new List(16); // --- Happiness Signal --- foreach (string raw in EventCatalog.Happiness.AuthoredIds) { string id = (raw ?? "").Trim(); if (string.IsNullOrEmpty(id)) { continue; } HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id); if (!EventCatalog.IsCameraWorthy(entry)) { continue; } result.HappinessAttempted++; HappinessEventRouter.ResetCountersForSubject(subjectId); InterestDropLog.Clear(); bool applied = HappinessGameApi.TryChangeHappiness(unit, id, 0); string level; string notes; if (applied && HappinessEventRouter.NotesSinceClear > 0 && string.Equals(HappinessEventRouter.LastEffectId, id, StringComparison.OrdinalIgnoreCase)) { level = "id"; notes = "notes=" + HappinessEventRouter.NotesSinceClear; } else if (!applied && DropMentions("no_emotions_or_egg", id)) { level = "id_drop"; notes = "no_emotions_or_egg"; } else if (!applied && DropMentions("no_emotions_or_egg", "")) { // Suppressed without id in detail still counts as authored psychopath/egg path. level = "id_drop"; notes = "no_emotions_or_egg_generic"; } else { level = "fail"; notes = applied ? ("applied_but_no_note last=" + HappinessEventRouter.LastEffectId + " notes=" + HappinessEventRouter.NotesSinceClear + " drops=" + InterestDropLog.FormatRecent(3)) : ("apply_false drops=" + InterestDropLog.FormatRecent(3)); result.Failures++; fails.Add("happiness:" + id + " " + notes); } EventLiveCoverageLedger.Claim("happiness", id, level, "happiness_apply", notes); } // --- Status CreatesInterest gains --- foreach (string raw in EventCatalog.Status.AuthoredIds) { string id = (raw ?? "").Trim(); if (string.IsNullOrEmpty(id)) { continue; } StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id); if (!EventCatalog.IsCameraWorthy(entry)) { continue; } result.StatusAttempted++; StatusGameApi.TryFinishAll(unit); ActivityLog.ResetStatusCountersForSubject(subjectId); InterestDropLog.Clear(); bool applied = StatusGameApi.TryAddStatus(unit, id, overrideTimer: 8f); bool gained = applied && ActivityLog.StatusGainsSinceClear > 0 && string.Equals(ActivityLog.LastStatusGainId, id, StringComparison.OrdinalIgnoreCase); string level; string notes; if (gained) { level = "id"; notes = "gains=" + ActivityLog.StatusGainsSinceClear; } else if (!applied && DropMentions("createsInterest=false", id)) { level = "id_drop"; notes = "createsInterest=false"; } else if (applied && ActivityLog.StatusRefreshesSinceClear > 0 && ActivityLog.StatusGainsSinceClear == 0) { // Still had the status somehow - treat as unexpected for a cleared unit. level = "fail"; notes = "refresh_without_gain"; result.Failures++; fails.Add("status:" + id + " " + notes); } else if (!applied) { // Game refused addStatusEffect (species/asset/stacking gate). Authored gap, not a harness bug. level = "id_drop"; notes = "apply_false"; } else { level = "fail"; notes = "applied_but_no_gain last=" + ActivityLog.LastStatusGainId + " gains=" + ActivityLog.StatusGainsSinceClear + " refreshes=" + ActivityLog.StatusRefreshesSinceClear; result.Failures++; fails.Add("status:" + id + " " + notes); } EventLiveCoverageLedger.Claim( "status", id, level, "status_apply", notes + " busy_bypass=activity_log_primary"); StatusGameApi.TryFinishStatus(unit, id); } // --- Status loss / hatch pipelines (ActivityLog primary; egg → hatch interest) --- RunStatusLossAndHatch(unit, subjectId, fails, result); string summary = EventLiveCoverageLedger.Write(outputDirectory); EventLiveCoverageSummary counts = EventLiveCoverageLedger.Summarize(); result.Passed = result.Failures == 0 && counts.Fail == 0 && result.HappinessAttempted > 0 && result.StatusAttempted > 0; result.Detail = $"happiness={result.HappinessAttempted} status={result.StatusAttempted} failures={result.Failures} " + summary + (fails.Count > 0 ? (" first=" + fails[0] + (fails.Count > 1 ? " +" + (fails.Count - 1) : "")) : ""); return result; } private static void RunStatusLossAndHatch( Actor unit, long subjectId, List fails, EventLiveApplyLoopResult result) { bool prev = AgentHarness.ForceLiveEventFeeds; AgentHarness.ForceLiveEventFeeds = true; try { // Sample camera status: gain then finish → ActivityLog loss. string sampleId = null; foreach (string raw in EventCatalog.Status.AuthoredIds) { string id = (raw ?? "").Trim(); if (string.IsNullOrEmpty(id) || string.Equals(id, "egg", StringComparison.OrdinalIgnoreCase)) { continue; } StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id); if (!EventCatalog.IsCameraWorthy(entry)) { continue; } sampleId = id; break; } if (!string.IsNullOrEmpty(sampleId)) { StatusGameApi.TryFinishAll(unit); StatusGameApi.TryAddStatus(unit, sampleId, overrideTimer: 8f); ActivityLog.ResetStatusCountersForSubject(subjectId); InterestDropLog.Clear(); StatusGameApi.TryFinishStatus(unit, sampleId); bool lost = ActivityLog.StatusLossesSinceClear > 0 && string.Equals( ActivityLog.LastStatusLossId, sampleId, StringComparison.OrdinalIgnoreCase); if (lost) { EventLiveCoverageLedger.Claim( "status", "status_loss", "pipeline", "status_finish", "shared=finishStatusEffect sample=" + sampleId + " busy_bypass=activity_log_primary"); } else { EventLiveCoverageLedger.Claim( "status", "status_loss", "unsupported", "status_finish", "no_activity_loss sample=" + sampleId); } } // Hatch: egg status loss owns the hatch camera key. StatusGameApi.TryFinishAll(unit); InterestRegistry.ForceExpireContaining("hatch:", UnityEngine.Time.unscaledTime); ActivityLog.ResetStatusCountersForSubject(subjectId); InterestDropLog.Clear(); bool eggOn = StatusGameApi.TryAddStatus(unit, "egg", overrideTimer: 2f); StatusGameApi.TryFinishStatus(unit, "egg"); bool hatchKey = InterestRegistry.CountKeysContaining("hatch:") > 0; bool eggLoss = ActivityLog.StatusLossesSinceClear > 0 && string.Equals( ActivityLog.LastStatusLossId, "egg", StringComparison.OrdinalIgnoreCase); if (hatchKey) { EventLiveCoverageLedger.Claim( "status", "egg", "id", "status_egg_loss", "hatch_key busy_bypass=ForceLiveEventFeeds"); } else if (eggOn && eggLoss) { EventLiveCoverageLedger.Claim( "status", "egg", "pipeline", "status_finish", "shared=finishStatusEffect activity_loss_only"); } else if (!eggOn) { EventLiveCoverageLedger.Claim( "status", "egg", "id_drop", "status_apply", "egg_apply_false"); } else { result.Failures++; fails.Add("status:egg hatch_miss"); EventLiveCoverageLedger.Claim( "status", "egg", "fail", "status_egg_loss", "no_hatch_key"); } } finally { AgentHarness.ForceLiveEventFeeds = prev; } } private static bool DropMentions(string reason, string detailNeedle) { List lines = InterestDropLog.Snapshot(12); for (int i = 0; i < lines.Count; i++) { string line = lines[i] ?? ""; if (line.IndexOf(reason, StringComparison.OrdinalIgnoreCase) < 0) { continue; } if (string.IsNullOrEmpty(detailNeedle) || line.IndexOf(detailNeedle, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } }