322 lines
12 KiB
C#
322 lines
12 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Phase 1 live apply loops: every camera-worthy happiness Signal and status gain
|
|
/// through real <c>changeHappiness</c> / <c>addStatusEffect</c>. Claims into
|
|
/// <see cref="EventLiveCoverageLedger"/> (<c>id</c> / <c>id_drop</c> / <c>fail</c>).
|
|
/// Primary asserts are ActivityLog / router notes (Busy may skip interest).
|
|
/// </summary>
|
|
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<string>(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<string> 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<string> 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;
|
|
}
|
|
}
|