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